diff --git a/gzstatic/about.html b/gzstatic/about.html index 483625f..42cee7d 100644 --- a/gzstatic/about.html +++ b/gzstatic/about.html @@ -1 +1,105 @@ -OpenJBOD - About

About OpenJBOD

OpenJBOD is an open source hardware and software project by TheGuyDanish (GitHub).

The OpenJBOD Controller hardware is open source and licensed under the CERN OHL v2 - Permissive license.

The OpenJBOD Controller software is open source and licensed under the MIT license.

Third party licenses

OpenJBOD uses the following libraries:
ProjectLicense
MicroPythonMIT
MicrodotMIT
utemplateMIT
Pure CSSBSD
Blog Layout Examplezlib

Special mentions and acknowledgements

The OpenJBOD project is made possible by the tireless efforts of many people, projects and communities, including but not limited to:

+ + + + + + + OpenJBOD - About + + + + + +
+ + + + + + + + +
+
+

About OpenJBOD

+

OpenJBOD is an open source hardware and software project by TheGuyDanish (GitHub).

+

The OpenJBOD Controller hardware is open source and licensed under the CERN OHL v2 - Permissive license.

+

The OpenJBOD Controller software is open source and licensed under the MIT license.

+
+
+

Third party licenses

+

OpenJBOD uses the following libraries:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ProjectLicense
MicroPythonMIT
MicrodotMIT
utemplateMIT
Pure CSSBSD
Pure CSS Layout Exampleszlib
+
+
+

Special mentions and acknowledgements

+

The OpenJBOD project is made possible by the tireless efforts of many people, projects and communities, including but not limited to:

+ +
+
+
+
+
+ + + diff --git a/gzstatic/main.css b/gzstatic/main.css deleted file mode 100644 index 475c18f..0000000 --- a/gzstatic/main.css +++ /dev/null @@ -1 +0,0 @@ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}a{background-color:transparent}b{font-weight:bolder}button,input,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button{text-transform:none}[type=button],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}html{font-family:sans-serif}.pure-g{display:flex;flex-flow:row wrap;align-content:flex-start}.pure-u-1,.pure-u-1-3,.pure-u-1-4,.pure-u-3-4{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-4{width:25%}.pure-u-1-3{width:33.3333%}.pure-u-3-4{width:75%}.pure-u-1{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;user-select:none;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button:focus,.pure-button:hover{background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-primary,a.pure-button-primary{background-color:#0078e7;color:#fff}.pure-form input[type=number],.pure-form input[type=text],.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;box-sizing:border-box}.pure-form input[type=number]:focus,.pure-form input[type=text]:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form-aligned input,.pure-form-aligned textarea{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form .pure-input-1{width:100%}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=number],.pure-form input[type=text],.pure-form label{margin-bottom:.3em;display:block}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}@media screen and (min-width:48em){.pure-u-md-1-4,.pure-u-md-3-4{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-md-1-4{width:25%}.pure-u-md-3-4{width:75%}}#layout,.nav-list{padding:0}.brand-title,.content-subhead,.nav-item a{text-transform:uppercase}.nav-item a{background:0 0}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}a{text-decoration:none;color:#3d92c9}a:focus,a:hover{text-decoration:underline}.header{text-align:center;top:auto;margin:3em auto}.sidebar{background:#3d4f5d;color:#fff}.brand-tagline,.brand-title{margin:0}.brand-tagline{font-weight:300;color:#b0cadb}.nav-list{margin:0;list-style:none}.nav-item{display:inline-block;zoom:1}.nav-item a{border:2px solid #b0cadb;color:#fff;margin-top:1em;letter-spacing:.05em;font-size:85%}.nav-item a:focus,.nav-item a:hover{border:2px solid #3d92c9;text-decoration:none}.content-subhead{color:#aaa;border-bottom:1px solid #eee;padding:.4em 0;font-size:80%;font-weight:500;letter-spacing:.1em}.content{padding:2em 1em 0}.post{padding-bottom:2em}@media (min-width:48em){.content{padding:2em 3em 0;margin-left:25%}.header{margin:80% 2em 0;text-align:right}.sidebar{position:fixed;top:0;bottom:0}} \ No newline at end of file diff --git a/gzstatic/pure-min.css b/gzstatic/pure-min.css new file mode 100644 index 0000000..78497b1 --- /dev/null +++ b/gzstatic/pure-min.css @@ -0,0 +1,11 @@ +/*! +Pure v3.0.0 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/pure-css/pure/blob/master/LICENSE +*/ +/*! +normalize.css v | MIT License | https://necolas.github.io/normalize.css/ +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}b{font-weight:bolder}button,input,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}html{font-family:sans-serif}.pure-g{display:flex;flex-flow:row wrap;align-content:flex-start}.pure-u-1,.pure-u-1-3,.pure-u-1-4,.pure-u-3-4{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-4{width:25%}.pure-u-1-3{width:33.3333%}.pure-u-3-4{width:75%}.pure-u-1{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;user-select:none;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button:focus,.pure-button:hover{background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-primary,a.pure-button-primary{background-color:#0078e7;color:#fff}.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=time],.pure-form input[type=url],.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;box-sizing:border-box}.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form-aligned input,.pure-form-aligned textarea{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form .pure-input-1{width:100%}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=time],.pure-form input[type=url],.pure-form label{margin-bottom:.3em;display:block}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}}.pure-menu{box-sizing:border-box}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-item .pure-menu-item{display:block}.pure-menu-heading{color:#565d64}.pure-menu-link{color:#777}.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb} \ No newline at end of file diff --git a/gzstatic/style.css b/gzstatic/style.css new file mode 100644 index 0000000..6da4bca --- /dev/null +++ b/gzstatic/style.css @@ -0,0 +1,230 @@ +body { + color: #777; +} + +/* +Add transition to containers so they can push in and out. +*/ +#layout, +#menu, +.menu-link { + -webkit-transition: all 0.2s ease-out; + -moz-transition: all 0.2s ease-out; + -ms-transition: all 0.2s ease-out; + -o-transition: all 0.2s ease-out; + transition: all 0.2s ease-out; +} + +/* +This is the parent `
` that contains the menu and the content area. +*/ +#layout { + position: relative; + left: 0; + padding-left: 0; +} +/* +The content `
` is where all your content goes. +*/ +.content { + margin: 0 auto; + padding: 0 2em; + margin-bottom: 50px; + line-height: 1.6em; +} + +.header { + margin: 0; + color: #333; + text-align: center; + padding: 2.5em 2em 0; + border-bottom: 1px solid #eee; +} + .header h1 { + margin: 0.2em 0; + font-size: 3em; + font-weight: 300; + } + +.content-subhead { + /*margin: 50px 0 20px 0;*/ + font-weight: 300; + color: #888; +} + + + +/* +The `#menu` `
` is the parent `
` that contains the `.pure-menu` that +appears on the left side of the page. +*/ + +#menu { + margin-left: -150px; /* "#menu" width */ + width: 150px; + position: fixed; + top: 0; + left: 0; + bottom: 0; + z-index: 1000; /* so the menu or its navicon stays above all content */ + background: #191818; + overflow-y: auto; +} + /* + All anchors inside the menu should be styled like this. + */ + #menu a { + color: #999; + border: none; + padding: 0.6em 0 0.6em 0.6em; + } + + /* + Remove all background/borders, since we are applying them to #menu. + */ + #menu .pure-menu, + #menu .pure-menu ul { + border: none; + background: transparent; + } + + /* + Add that light border to separate items into groups. + */ + #menu .pure-menu ul { + border-top: 1px solid #333; + } + /* + Change color of the anchor links on hover/focus. + */ + #menu .pure-menu li a:hover, + #menu .pure-menu li a:focus { + background: #333; + } + + /* + This styles the selected menu item `
  • `. + */ + + #menu .pure-menu-heading { + background: #1f8dd6; + } + /* + This styles a link within a selected menu item `
  • `. + */ + + /* + This styles the menu heading. + */ + #menu .pure-menu-heading { + font-size: 110%; + color: #fff; + margin: 0; + } + +/* -- Dynamic Button For Responsive Menu -------------------------------------*/ + +/* +The button to open/close the Menu is custom-made and not part of Pure. Here's +how it works: +*/ + +/* +`.menu-link` represents the responsive menu toggle that shows/hides on +small screens. +*/ +.menu-link { + position: fixed; + display: block; /* show this only on small screens */ + top: 0; + left: 0; /* "#menu width" */ + background: #000; + background: rgba(0,0,0,0.7); + font-size: 10px; /* change this value to increase/decrease button size */ + z-index: 10; + width: 2em; + height: auto; + padding: 2.1em 1.6em; +} + + .menu-link:hover, + .menu-link:focus { + background: #000; + } + + .menu-link span { + position: relative; + display: block; + } + + .menu-link span, + .menu-link span:before, + .menu-link span:after { + background-color: #fff; + pointer-events: none; + width: 100%; + height: 0.2em; + } + + .menu-link span:before, + .menu-link span:after { + position: absolute; + margin-top: -0.6em; + content: " "; + } + + .menu-link span:after { + margin-top: 0.6em; + } + + +/* -- Responsive Styles (Media Queries) ------------------------------------- */ + +/* +Hides the menu at `48em`, but modify this based on your app's needs. +*/ +@media (min-width: 48em) { + + .header, + .content { + padding-left: 2em; + padding-right: 2em; + } + + #layout { + padding-left: 150px; /* left col width "#menu" */ + left: 0; + } + #menu { + left: 150px; + } + + .menu-link { + position: fixed; + left: 150px; + display: none; + } +} + +@media (max-width: 48em) { + /* Only apply this when the window is small. Otherwise, the following + case results in extra padding on the left: + * Make the window small. + * Tap the menu to trigger the active state. + * Make the window large again. + */ +} + +.content-subhead { + text-transform: uppercase; + color: #aaa; + border-bottom: 1px solid #eee; + padding: 0.4em 0; + font-size: 80%; + font-weight: 500; + letter-spacing: 0.1em; +} + +.mt10 { + margin-top:10px; +} \ No newline at end of file diff --git a/helpers.py b/helpers.py index bb45bf4..5822675 100644 --- a/helpers.py +++ b/helpers.py @@ -5,9 +5,43 @@ import machine import time import binascii +from hashlib import sha1 CONFIG_FILE = "config.json" +class SRLatch: + def __init__(self, set_pin: machine.Pin, reset_pin: machine.Pin, + sense_pin: machine.Pin): + if (not isinstance(set_pin, machine.Pin) or + not isinstance(reset_pin, machine.Pin) or + not isinstance(sense_pin, machine.Pin)): + raise TypeError("All arguments must be machine.Pin instances.") + + self.set_pin = set_pin + self.reset_pin = reset_pin + self.sense_pin = sense_pin + + def on(self): + self.reset_pin.off() + self.set_pin.on() + time.sleep_ms(250) + self.set_pin.off() + + def off(self): + self.set_pin.off() + self.reset_pin.on() + time.sleep_ms(250) + self.reset_pin.off() + + def state(self): + if self.sense_pin.value(): + return True + else: + return False + +def create_hash(password): + return binascii.hexlify(sha1(password).digest()).decode("utf-8") + def get_id(): # Uses the flash attached to the RP2040 to derive a unique board ID. return binascii.hexlify(machine.unique_id()).decode('utf-8').upper() @@ -35,20 +69,6 @@ def get_mac_address(spi, cs): return mac_str -def psu_on(lset, lreset): - # Turn off the latch reset pin (it shouldn't be on anyway), then turn on the set pin briefly. - lreset.off() - lset.on() - time.sleep(0.2) - lset.off() - -def psu_off(lset, lreset): - # Opposite of psu_on() - lset.off() - lreset.on() - time.sleep(0.2) - lreset.off() - def reset_rp2040(): # Note, this is a hard reset. For soft resets, use sys.exit() machine.reset() @@ -57,21 +77,22 @@ def get_rp2040_temp(): conversion_factor = 3.3/(65535) reading = machine.ADC(4).read_u16() * conversion_factor - temp = round(27 - (reading - 0.706)/0.001721) + temp = 27 - (reading - 0.706)/0.001721 return temp def get_ds18x20_temp(ds_sensor, rom): ds_sensor.convert_temp() time.sleep_ms(250) - return round(ds_sensor.read_temp(rom)) + return ds_sensor.read_temp(rom) def check_temp(temp, fan_curve): + fan_speed = list(fan_curve.values())[0]['fan_p'] for step in fan_curve.values(): if temp >= step['temp']: fan_speed = step['fan_p'] else: - fan_speed = fan_curve['1']['fan_p'] + break return fan_speed def get_network_info(ifconfig): diff --git a/lib/microdot/auth.py b/lib/microdot/auth.py new file mode 100644 index 0000000..1e384c5 --- /dev/null +++ b/lib/microdot/auth.py @@ -0,0 +1,144 @@ +from microdot import abort +from microdot.microdot import invoke_handler + + +class BaseAuth: + def __init__(self): + self.auth_callback = None + self.error_callback = None + + def __call__(self, f): + """Decorator to protect a route with authentication. + + An instance of this class must be used as a decorator on the routes + that need to be protected. Example:: + + auth = BasicAuth() # or TokenAuth() + + @app.route('/protected') + @auth + def protected(request): + # ... + + Routes that are decorated in this way will only be invoked if the + authentication callback returned a valid user object, otherwise the + error callback will be executed. + """ + async def wrapper(request, *args, **kwargs): + auth = self._get_auth(request) + if not auth: + return await invoke_handler(self.error_callback, request) + request.g.current_user = await invoke_handler( + self.auth_callback, request, *auth) + if not request.g.current_user: + return await invoke_handler(self.error_callback, request) + return await invoke_handler(f, request, *args, **kwargs) + + return wrapper + + +class BasicAuth(BaseAuth): + """Basic Authentication. + + :param realm: The realm that is displayed when the user is prompted to + authenticate in the browser. + :param charset: The charset that is used to encode the realm. + :param scheme: The authentication scheme. Defaults to 'Basic'. + :param error_status: The error status code to return when authentication + fails. Defaults to 401. + """ + def __init__(self, realm='Please login', charset='UTF-8', scheme='Basic', + error_status=401): + super().__init__() + self.realm = realm + self.charset = charset + self.scheme = scheme + self.error_status = error_status + self.error_callback = self.authentication_error + + def _get_auth(self, request): + auth = request.headers.get('Authorization') + if auth and auth.startswith('Basic '): + import binascii + try: + username, password = binascii.a2b_base64( + auth[6:]).decode().split(':', 1) + except Exception: # pragma: no cover + return None + return username, password + + def authentication_error(self, request): + return '', self.error_status, { + 'WWW-Authenticate': '{} realm="{}", charset="{}"'.format( + self.scheme, self.realm, self.charset)} + + def authenticate(self, f): + """Decorator to configure the authentication callback. + + This decorator must be used with a function that accepts the request + object, a username and a password and returns a user object if the + credentials are valid, or ``None`` if they are not. Example:: + + @auth.authenticate + async def check_credentials(request, username, password): + user = get_user(username) + if user and user.check_password(password): + return get_user(username) + """ + self.auth_callback = f + + +class TokenAuth(BaseAuth): + """Token based authentication. + + :param header: The name of the header that will contain the token. Defaults + to 'Authorization'. + :param scheme: The authentication scheme. Defaults to 'Bearer'. + :param error_status: The error status code to return when authentication + fails. Defaults to 401. + """ + def __init__(self, header='Authorization', scheme='Bearer', + error_status=401): + super().__init__() + self.header = header + self.scheme = scheme.lower() + self.error_status = error_status + self.error_callback = self.authentication_error + + def _get_auth(self, request): + auth = request.headers.get(self.header) + if auth: + if self.header == 'Authorization': + try: + scheme, token = auth.split(' ', 1) + except Exception: + return None + if scheme.lower() == self.scheme: + return (token.strip(),) + else: + return (auth,) + + def authenticate(self, f): + """Decorator to configure the authentication callback. + + This decorator must be used with a function that accepts the request + object, a username and a password and returns a user object if the + credentials are valid, or ``None`` if they are not. Example:: + + @auth.authenticate + async def check_credentials(request, token): + return get_user(token) + """ + self.auth_callback = f + + def errorhandler(self, f): + """Decorator to configure the error callback. + + Microdot calls the error callback to allow the application to generate + a custom error response. The default error response is to call + ``abort(401)``. + """ + self.error_callback = f + + def authentication_error(self, request): + abort(self.error_status) \ No newline at end of file diff --git a/lib/microdot/login.py b/lib/microdot/login.py new file mode 100644 index 0000000..65b0287 --- /dev/null +++ b/lib/microdot/login.py @@ -0,0 +1,150 @@ +from time import time +from microdot import redirect +from microdot.microdot import urlencode, invoke_handler + + +class Login: + def __init__(self, login_url='/login'): + self.login_url = login_url + self.user_callback = None + self.user_id_callback = lambda user: user.id + + def id_to_user(self, f): + """Decorator to configure the user callback. + + The decorated function receives the user ID as an argument and must + return the corresponding user object, or ``None`` if the user ID is + invalid. + """ + self.user_callback = f + + def user_to_id(self, f): + """Decorator to configure the user ID callback. + + The decorated functon receives the user object as an argument and must + return the corresponding user ID. By default, the ``id`` attribute of + the user object is used. + """ + self.user_id_callback = f + + def _get_session(self, request): + return request.app._session.get(request) + + def _update_remember_cookie(self, request, days, user_id=None): + remember_payload = request.app._session.encode({ + 'user_id': user_id, + 'days': days, + 'exp': time() + days * 24 * 60 * 60 + }) + + @request.after_request + async def _set_remember_cookie(request, response): + response.set_cookie('_remember', remember_payload, + max_age=days * 24 * 60 * 60) + return response + + def _get_user_id_from_session(self, request): + session = self._get_session(request) + if session and '_user_id' in session: + return session['_user_id'] + if '_remember' in request.cookies: + remember_payload = request.app._session.decode( + request.cookies['_remember']) + user_id = remember_payload.get('user_id') + if user_id: # pragma: no branch + self._update_remember_cookie( + request, remember_payload.get('_days', 30), user_id) + session['_user_id'] = user_id + session['_fresh'] = False + session.save() + return user_id + + async def _redirect_to_login(self, request): + return '', 302, {'Location': self.login_url + '?next=' + urlencode( + request.url)} + + async def login_user(self, request, user, remember=False, + redirect_url='/'): + """Log a user in. + + :param request: the request object + :param user: the user object + :param remember: if the user's logged in state should be remembered + with a cookie after the session ends. Set to the + number of days the remember cookie should last, or to + ``True`` to use a default duration of 30 days. + :param redirect_url: the URL to redirect to after login + + This call marks the user as logged in by storing their user ID in the + user session. The application must call this method to log a user in + after their credentials have been validated. + + The method returns a redirect response, either to the URL the user + originally intended to visit, or if there is no original URL to the URL + specified by the `redirect_url`. + """ + session = self._get_session(request) + session['_user_id'] = await invoke_handler(self.user_id_callback, user) + session['_fresh'] = True + session.save() + + if remember: + days = 30 if remember is True else int(remember) + self._update_remember_cookie(request, days, session['_user_id']) + + next_url = request.args.get('next', redirect_url) + if not next_url.startswith('/'): + next_url = redirect_url + return redirect(next_url) + + async def logout_user(self, request): + """Log a user out. + + :param request: the request object + + This call removes information about the user's log in from the user + session. If a remember cookie exists, it is removed as well. + """ + session = self._get_session(request) + session.pop('_user_id', None) + session.pop('_fresh', None) + session.save() + if '_remember' in request.cookies: + self._update_remember_cookie(request, 0) + + def __call__(self, f): + """Decorator to protect a route with authentication. + + If the user is not logged in, Microdot will redirect to the login page + first. The decorated route will only run after successful login by the + user. If the user is already logged in, the route will run immediately. + """ + async def wrapper(request, *args, **kwargs): + user_id = self._get_user_id_from_session(request) + if not user_id: + return await self._redirect_to_login(request) + request.g.current_user = await invoke_handler( + self.user_callback, user_id) + if not request.g.current_user: + return await self._redirect_to_login(request) + return await invoke_handler(f, request, *args, **kwargs) + + return wrapper + + def fresh(self, f): + """Decorator to protect a route with "fresh" authentication. + + This decorator prevents the route from running when the login session + is not fresh. A fresh session is a session that has been created from + direct user interaction with the login page, as opposite to a session + that was restored from a "remember me" cookie. + """ + base_wrapper = self.__call__(f) + + async def wrapper(request, *args, **kwargs): + session = self._get_session(request) + if session.get('_fresh'): + return await base_wrapper(request, *args, **kwargs) + return await self._redirect_to_login(request) + + return wrapper \ No newline at end of file diff --git a/main.py b/main.py index de373d2..9936f45 100644 --- a/main.py +++ b/main.py @@ -2,25 +2,27 @@ from time import sleep from emc2301.emc2301 import EMC2301 import helpers -import onewire, ds18x20 import network import time import os import _thread +import onewire, ds18x20 from microdot import Microdot, Response, send_file, redirect from microdot.utemplate import Template from microdot.session import Session, with_session +from microdot.auth import BasicAuth import sys +VERSION = "1.0.0" DEBUG = True DEFAULT_CONFIG = { 'network': { 'hostname': "openjbod", 'method': "dhcp", - 'ip': None, - 'subnet_mask': None, - 'gateway': None, - 'dns': None + 'ip': "", + 'subnet_mask': "", + 'gateway': "", + 'dns': "" }, 'power': { 'on_boot': False, @@ -30,16 +32,28 @@ 'ignore_power_switch': False }, 'monitoring': { - 'use_ds18x20': False, + 'use_ds18x20': True, + 'use_ext_probe': False, + 'ignore_fan_fail': False + }, + 'web': { + 'use_tls': False, + 'users': { + '1': {'username': 'admin', 'password': 'c37a6c962da994da14d7494769ff5d53aac6eaf0'}, #admin/openjbod + '2': {'username': '', 'password': ''}, + '3': {'username': '', 'password': ''}, + '4': {'username': '', 'password': ''}, + '5': {'username': '', 'password': ''} + } }, 'fan_curve': { - '1': {'temp': 16, 'fan_p': 20}, - '2': {'temp': 25, 'fan_p': 40}, - '3': {'temp': 35, 'fan_p': 60}, - '4': {'temp': 45, 'fan_p': 80}, - '5': {'temp': 60, 'fan_p': 100} + '1': {'temp': 10, 'fan_p': 20}, + '2': {'temp': 20, 'fan_p': 40}, + '3': {'temp': 30, 'fan_p': 60}, + '4': {'temp': 40, 'fan_p': 80}, + '5': {'temp': 50, 'fan_p': 100} }, - 'notes': 'This field can be used to store free-form notes about the device, such as where it is located, or what it is connected.' + 'notes': 'This field can be used to store free-form notes about the device, such as where it is located, or what it is connected to.' } try: @@ -50,55 +64,73 @@ print("[INIT] Config not found, writing and assuming defaults!") helpers.write_config(DEFAULT_CONFIG) CONFIG = helpers.read_config() + +# SSL is unstable. +# See https://github.com/OpenJBOD/software/issues/1 +if CONFIG['web']['use_tls']: + import ssl + try: + sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + sslctx.load_cert_chain('cert.der', 'key.der') + print("[INIT] Loaded SSL Context!") + except ValueError as e: + print("[INIT] Value error! Reverting to non-SSL!") + print(e) + CONFIG['web']['use_tls'] = False + except OSError: + print("[INIT] No SSL certs! Reverting to non-SSL!") + CONFIG['web']['use_tls'] = False def fan_fail_handler(pin): - # TODO: See https://github.com/OpenJBOD/rp2040/issues/3 - print("FAN FAILED!") + # TODO: See https://github.com/OpenJBOD/software/issues/3 + FAN_FAILED = True def power_btn_handler(pin): - #REWRITE FOR REV4 - if psu_set.value(): - psu_set.off() + if psu.state(): + psu.off() else: - psu_set.on() + psu.on() # CHECK THESE FOR REV4 # Busses -i2c = I2C(1, scl=Pin(19), sda=Pin(18), freq=100000) +i2c = I2C(0, scl=Pin(9), sda=Pin(8), freq=100000) spi = SPI(0, 2000000, mosi=Pin(3), miso=Pin(4), sck=Pin(2)) -onew = Pin(27) +onew = Pin(18) # On-board probe. +if CONFIG['monitoring']['use_ext_probe']: + onew = Pin(11) # External probe header. # Individual pin functions -#led = Pin(6, Pin.OUT) -led = Pin(11, Pin.OUT) -#fan_fail = Pin(13, Pin.IN, Pin.PULL_UP) -power_btn = Pin(28, Pin.IN, Pin.PULL_UP) -#psu_reset = Pin(13) -psu_set = Pin(29, Pin.OUT) -#psu_sense = Pin(15, Pin.IN) -#usb_sense = Pin(25, Pin.IN) +led = Pin(6, Pin.OUT) +fan_fail = Pin(10, Pin.IN, Pin.PULL_UP) +power_btn = Pin(12, Pin.IN, Pin.PULL_UP) +psu_reset = Pin(13, Pin.OUT) +psu_set = Pin(14, Pin.OUT) +psu_sense = Pin(15, Pin.IN) +usb_sense = Pin(25, Pin.IN) # Interrupts power_btn.irq(trigger=Pin.IRQ_FALLING,handler=power_btn_handler) -#fan_fail.irq(trigger=Pin.IRQ_FALLING,handler=fan_fail_handler) +fan_fail.irq(trigger=Pin.IRQ_FALLING,handler=fan_fail_handler) + +psu = helpers.SRLatch(psu_set, psu_reset, psu_sense) led.on() emc2301 = EMC2301(i2c) # This shouldn't need to be hardcoded. -# See https://github.com/OpenJBOD/rp2040/issues/4 +# 3 is a good default for NF-F12 fans. +# See https://github.com/OpenJBOD/software/issues/4 emc2301.set_fan_edges(3) -if CONFIG['monitoring']['use_ds18x20']: - ds_sensor = ds18x20.DS18X20(onewire.OneWire(onew)) - ds_roms = ds_sensor.scan() - if len(ds_roms) == 0: - print("[INIT] No ds18x20 device found, reverting to RP2040 measurements") - CONFIG['monitoring']['use_ds18x20'] = False - else: - ds_rom = ds_roms[0] +ds_sensor = ds18x20.DS18X20(onewire.OneWire(onew)) +ds_roms = ds_sensor.scan() +if len(ds_roms) == 0: + print("[INIT] No ds18x20 device found, reverting to RP2040 measurements") + CONFIG['monitoring']['use_ds18x20'] = False +else: + ds_rom = ds_roms[0] def w5500_init(spi): - nic = network.WIZNET5K(spi, Pin(5), Pin(0)) + nic = network.WIZNET5K(spi, Pin(5), Pin(0)) # Bus, CSn, RSTn # Setting the hostname currently does nothing. - # See https://github.com/OpenJBOD/rp2040/issues/2 + # See https://github.com/OpenJBOD/software/issues/2 network.hostname(CONFIG['network']['hostname']) nic.active(True) if CONFIG['network']['method'] == "static": @@ -109,66 +141,126 @@ def w5500_init(spi): nic.ifconfig((ip_addr, subnet_mask, gateway, dns)) while not nic.isconnected(): sleep(1) - print("[INIT]: Not connected to NIC, waiting...") + print("[INIT] Not connected to NIC, waiting for IP...") return nic.ifconfig() -# Get mac before init'ing w5500 so we don't mess with it. -mac_addr = helpers.get_mac_address(spi, Pin(5)) ifconfig = w5500_init(spi) +MAC_ADDR = helpers.get_mac_address(spi, Pin(5)) +print(ifconfig) def temp_monitor(): while True: - # Currently broken, unsure why. - # Needs debugging in the main/core0 thread. - if CONFIG['monitoring']['use_ds18x20']: - temp = helpers.get_ds18x20_temp(ds_sensor, ds_rom) + if psu_sense.value(): + if CONFIG['monitoring']['use_ds18x20']: + temp = helpers.get_ds18x20_temp(ds_sensor, ds_rom) + else: + temp = helpers.get_rp2040_temp() + fan_p = helpers.check_temp(temp, CONFIG['fan_curve']) + duty_cycle = helpers.percent_to_duty(fan_p) + emc2301.set_pwm_duty_cycle(duty_cycle) else: - temp = helpers.get_rp2040_temp() - fan_p = helpers.check_temp(temp, CONFIG['fan_curve']) - duty_cycle = helpers.percent_to_duty(fan_p) - emc2301.set_pwm_duty_cycle(duty_cycle) - time.sleep(60) + continue + time.sleep(20) def webserver(): app = Microdot() + auth = BasicAuth() Response.default_content_type = 'text/html' # Utility + @auth.authenticate + async def check_credentials(request, username, password): + for user in CONFIG['web']['users']: + if username in CONFIG['web']['users'][user]['username']: + if helpers.create_hash(password) == CONFIG['web']['users'][user]['password']: + return user + @app.route('/static/') + @auth def static(request, path): if '..' in path: return 'Not found', 404 return send_file('gzstatic/' + path, compressed=True, file_extension='.gz') @app.route('/power_toggle') + @auth async def power_toggle(req): - psu_set.toggle() + if psu.state(): + psu.off() + else: + psu.on() + return redirect('/') + + @app.route('/reset_rp2040') + @auth + async def reset_rp2040(req): + helpers.reset_rp2040() return redirect('/') @app.route('/reset_config') + @auth async def reset_config(req): helpers.write_config(DEFAULT_CONFIG) helpers.reset_rp2040() @app.route('/note', methods=['POST']) + @auth async def update_note(req): CONFIG['notes'] = req.form['notes'] helpers.write_config(CONFIG) return redirect('/') + @app.route('/upload/cert', methods=['POST']) + @auth + async def upload_cert(req): + filename = req.headers['Content-Disposition'].split( + 'filename=')[1].strip('"') + size = int(req.headers['Content-Length']) + filename = "cert.der" + + with open(filename, 'wb') as f: + while size > 0: + chunk = await req.stream.read(min(size, 1024)) + f.write(chunk) + size -= len(chunk) + + return '' + + @app.route('/upload/key', methods=['POST']) + @auth + async def upload_key(req): + filename = req.headers['Content-Disposition'].split( + 'filename=')[1].strip('"') + size = int(req.headers['Content-Length']) + filename = "key.der" + + with open(filename, 'wb') as f: + while size > 0: + chunk = await req.stream.read(min(size, 1024)) + f.write(chunk) + size -= len(chunk) + + return '' + # Pages - @app.route('/settings', methods=['GET', 'POST']) - async def settings_page(req): - if req.method == 'POST': - print(req.form) - # TODO: Validation logic - # https://github.com/OpenJBOD/rp2040/issues/5 + @app.route('/settings/network', methods=['GET', 'POST']) + @auth + async def settings_network(req): + if req.method == "POST": CONFIG['network']['hostname'] = req.form['hostname'] CONFIG['network']['method'] = req.form['ip_method'] CONFIG['network']['ip'] = req.form['ip_address'] CONFIG['network']['subnet_mask'] = req.form['subnet_mask'] CONFIG['network']['gateway'] = req.form['gateway_ip'] CONFIG['network']['dns'] = req.form['dns_ip'] + helpers.write_config(CONFIG) + return redirect('/settings/network') + return Template('settings_network.html').render(config=CONFIG) + + @app.route('/settings/power', methods=['GET', 'POST']) + @auth + async def settings_power(req): + if req.method == "POST": if req.form.get('on_boot'): CONFIG['power']['on_boot'] = True else: @@ -183,10 +275,24 @@ async def settings_page(req): CONFIG['power']['ignore_power_switch'] = True else: CONFIG['power']['ignore_power_switch'] = False - if req.form.get('use_ds18x20'): - CONFIG['monitoring']['use_ds18x20'] = True + helpers.write_config(CONFIG) + return redirect('/settings/power') + return Template('settings_power.html').render(config=CONFIG) + + @app.route('/settings/environment', methods=['GET', 'POST']) + @auth + async def settings_environ(req): + if req.method == "POST": + if req.form.get('use_ext_probe'): + old_ds18x20 = CONFIG['monitoring']['use_ext_probe'] + CONFIG['monitoring']['use_ext_probe'] = True else: - CONFIG['monitoring']['use_ds18x20'] = False + old_ds18x20 = CONFIG['monitoring']['use_ext_probe'] + CONFIG['monitoring']['use_ext_probe'] = False + if req.form.get('ignore_fan_fail'): + CONFIG['monitoring']['ignore_fan_fail'] = True + else: + CONFIG['monitoring']['ignore_fan_fail'] = False CONFIG['fan_curve']['1']['temp'] = int(req.form['curve_1_c']) CONFIG['fan_curve']['1']['fan_p'] = int(req.form['curve_1_p']) CONFIG['fan_curve']['2']['temp'] = int(req.form['curve_2_c']) @@ -197,36 +303,95 @@ async def settings_page(req): CONFIG['fan_curve']['4']['fan_p'] = int(req.form['curve_4_p']) CONFIG['fan_curve']['5']['temp'] = int(req.form['curve_5_c']) CONFIG['fan_curve']['5']['fan_p'] = int(req.form['curve_5_p']) - CONFIG['notes'] = req.form['notes'] helpers.write_config(CONFIG) - redirect('/') - helpers.reset_rp2040() - return Template('settings.html').render(config=CONFIG) + # Revert to the previous setting after writing config. + # This is to avoid having to re-do the sensor setup code. + # Instead, the used probe will change on uC reset. + if old_ds18x20 == False: + CONFIG['monitoring']['use_ext_probe'] = False + return redirect('/settings/environment') + return Template('settings_environment.html').render(config=CONFIG) + + @app.route('/settings/tls', methods=['GET', 'POST']) + @auth + async def settings_tls(req): + if req.method == "POST": + if req.form.get('use_tls'): + CONFIG['web']['use_tls'] = True + else: + CONFIG['web']['use_tls'] = False + helpers.write_config(CONFIG) + return redirect('/settings/tls') + return Template('settings_tls.html').render(config=CONFIG) + + @app.route('/settings/users', methods=['GET', 'POST']) + @auth + async def settings_users(req): + if req.method == "POST": + if req.form.get("user_1_n") != CONFIG['web']['users']['1']['username']: + CONFIG['web']['users']['1']['username'] = req.form['user_1_n'] + if req.form.get('user_1_cp'): + CONFIG['web']['users']['1']['password'] = helpers.create_hash(req.form['user_1_p']) + if req.form.get("user_2_n") != CONFIG['web']['users']['2']['username']: + CONFIG['web']['users']['2']['username'] = req.form['user_2_n'] + if req.form.get('user_2_cp'): + CONFIG['web']['users']['2']['password'] = helpers.create_hash(req.form['user_2_p']) + if req.form.get("user_3_n") != CONFIG['web']['users']['3']['username']: + CONFIG['web']['users']['3']['username'] = req.form['user_3_n'] + if req.form.get('user_3_cp'): + CONFIG['web']['users']['3']['password'] = helpers.create_hash(req.form['user_3_p']) + if req.form.get("user_4_n") != CONFIG['web']['users']['4']['username']: + CONFIG['web']['users']['4']['username'] = req.form['user_4_n'] + if req.form.get('user_4_cp'): + CONFIG['web']['users']['4']['password'] = helpers.create_hash(req.form['user_4_p']) + if req.form.get("user_5_n") != CONFIG['web']['users']['5']['username']: + CONFIG['web']['users']['5']['username'] = req.form['user_5_n'] + if req.form.get('user_5_cp'): + CONFIG['web']['users']['5']['password'] = helpers.create_hash(req.form['user_5_p']) + + helpers.write_config(CONFIG) + return redirect('/settings/users') + return Template('settings_users.html').render(config=CONFIG) + + @app.route('/settings/reset') + @auth + async def settings_users(req): + return Template('settings_reset.html').render() @app.route('/') @app.route('/index') + @auth async def index(req): response = {} response['config'] = CONFIG - response['atx_state'] = psu_set.value() + response['atx_state'] = psu.state() response['serial'] = helpers.get_id() response['fan_rpm'] = emc2301.get_fan_speed(edges=3, poles=1) response['net_info'] = helpers.get_network_info(ifconfig) - response['mac_addr'] = mac_addr - response['duty_cycle'] = helpers.duty_to_percent(emc2301.get_pwm_duty_cycle()) + response['mac_addr'] = MAC_ADDR + response['fan_speed_p'] = helpers.duty_to_percent(emc2301.get_pwm_duty_cycle()) + response['version'] = VERSION if CONFIG['monitoring']['use_ds18x20']: - response['temp'] = helpers.get_ds18x20_temp(ds_sensor, ds_rom) + response['temp'] = round(helpers.get_ds18x20_temp(ds_sensor, ds_rom), 2) else: - response['temp'] = helpers.get_rp2040_temp() + response['temp'] = round(helpers.get_rp2040_temp(), 2) return Template('index.html').render(resp=response) @app.route('/about') + @auth async def about(req): return send_file('gzstatic/about.html', compressed=True, file_extension='.gz') - # TODO: SSL/TLS support - # https://github.com/OpenJBOD/rp2040/issues/1 - app.run(port=80) + if CONFIG['web']['use_tls']: + try: + app.run(port=443, debug=True, ssl=sslctx) + except OSError as e: + print("Unable to start SSL stream, reverting to non-SSL") + print(e) + app.run(port=80, debug=True) + else: + app.run(port=80, debug=True) -fan = _thread.start_new_thread(temp_monitor, ()) +_thread.start_new_thread(temp_monitor, ()) webserver() + diff --git a/templates/index.html b/templates/index.html index e839f71..2f0ba14 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,2 +1,113 @@ {% args resp %} -OpenJBOD

    Overview

    Power Supply{% if resp['atx_state'] %}

    State: On{% else %}

    State: Off{% endif %}

    Turn On/Off
    Environment Info

    Fan RPM: {{ resp['fan_rpm'] }}

    Target Fan Speed: {{ resp['duty_cycle'] }}%

    Temperature: {{ resp['temp'] }}

    Serial Number

    {{ resp['serial'] }}


    Network Status
    Hostname{{ resp['config']['network']['hostname'] }}
    IP Config Method{{ resp['config']['network']['method'] }}
    IP Address{{ resp['net_info']['ip_addr'] }}
    Subnet Mask{{ resp['net_info']['subnet_mask'] }}
    Gateway{{ resp['net_info']['gateway_ip'] }}
    DNS Server{{ resp['net_info']['dns_ip'] }}
    MAC Address{{ resp['mac_addr'] }}
    Power Settings

    On Boot: {{ resp['config']['power']['on_boot'] }}

    On Boot Delay: {{ resp['config']['power']['on_boot_delay'] }}

    Follow USB: {{ resp['config']['power']['follow_usb'] }}

    Follow USB Delay: {{ resp['config']['power']['follow_usb_delay'] }}

    Ignore Power Switch: {{ resp['config']['power']['ignore_power_switch'] }}

    Notes

    + + + + + + + OpenJBOD + + + + + +
    + + + + + + + + +
    +
    +

    Overview

    +
    +
    +

    Power Supply

    +

    State: {% if resp['atx_state'] %}On{% else %}Off{% endif %}

    + Turn On/Off +
    +
    +

    Environment Info

    +

    Fan RPM: {{ resp['fan_rpm'] }}

    +

    Fan Speed: {{ resp['fan_speed_p'] }}%

    +

    Temperature: {{ resp['temp'] }} C

    +
    +
    +

    Board Info

    +

    Serial Number: {{ resp['serial'] }}

    +

    Software Version: {{ resp['version'] }}

    +

    MAC Address: {{ resp['mac_addr'] }}

    +
    +
    +
    +
    +

    Network Info

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    Hostname{{ resp['config']['network']['hostname'] }}
    IP Config Method{{ resp['config']['network']['method'] }}
    IP Address{{ resp['net_info']['ip_addr'] }}
    Subnet Mask{{ resp['net_info']['subnet_mask'] }}
    Gateway{{ resp['net_info']['gateway_ip'] }}
    DNS Server{{ resp['net_info']['dns_ip'] }}
    +
    +
    +

    Power Settings

    +

    On Boot: {{ resp['config']['power']['on_boot'] }}

    +

    On Boot Delay: {{ resp['config']['power']['on_boot_delay'] }}

    +

    Follow USB: {{ resp['config']['power']['follow_usb'] }}

    +

    Follow USB Delay: {{ resp['config']['power']['follow_usb_delay'] }}

    +

    Ignore Power Switch: {{ resp['config']['power']['ignore_power_switch'] }}

    +
    +
    +

    Notes

    +
    + +
    + +
    +
    +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/templates/settings.html b/templates/settings.html deleted file mode 100644 index 74d0fd2..0000000 --- a/templates/settings.html +++ /dev/null @@ -1,31 +0,0 @@ -{% args config %} -OpenJBOD - Settings

    Settings

    -
    -
    -

    Networking

    -
    -
    - -

    -
    -
    -
    -

    Power

    - -
    -
    -
    -

    Notes

    -

    Monitoring

    - -

    Fan Curve

    -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    Save

    Warning: Upon saving, the processor will reset to apply the new settings. Please beware of this, so as to not lose disk access.

    Warning: Resetting the configuration will default OpenJBOD to DHCP, meaning you may lose your access. Please beware of this before resetting the configuration.

    Reset to Defaults
    diff --git a/templates/settings_environment.html b/templates/settings_environment.html new file mode 100644 index 0000000..d873688 --- /dev/null +++ b/templates/settings_environment.html @@ -0,0 +1,116 @@ +{% args config %} + + + + + + + OpenJBOD - Network + + + + + +
    + + + + + + + + +
    +
    +
    +

    Environment

    +
    +
    +
    + + +
    +
    + + +
    +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    + +

    Note: After saving the settings, the device will need to restart to apply them. The board can be restarted on the Reset page.

    +
    +
    +
    +
    +

    Legend

    +
    +

    Use External Probe - Setting this checkbox will cause the software to use the external probe attached to the J9 header.

    +

    Ignore Fan Failures - Currently unimplemented, see this issue.

    +

    Fan Curve - There are five built-in steps into this software's fan curve. When the temperature reading reaches the threshold in one of the steps, the current fan speed will be adjusted to the defined percentage for that step.

    +
    +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/templates/settings_network.html b/templates/settings_network.html new file mode 100644 index 0000000..4fcb870 --- /dev/null +++ b/templates/settings_network.html @@ -0,0 +1,95 @@ +{% args config %} + + + + + + + OpenJBOD - Network + + + + + + +
    + + + + + + + + +
    +
    +
    +

    Networking

    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +

    Note: After saving the settings, the device will need to restart to apply them. The board can be restarted on the Reset page.

    +
    +
    +
    +
    +

    Legend

    +
    +

    Hostname - This option currently has no effect. Please see this issue.

    +

    IP Method - Decides whether to use DHCP or static IP addressing. Addresses input while the controller is in DHCP mode will still be saved, but not used.

    +

    IP Address - IPv4 Address of the device, only used with the static option above.

    +

    Subnet Mask - IPv4 subnet mask of the device, only used with the static option above.

    +

    Gateway - IPv4 gateway of the device, only used with the static option above.

    +

    DNS - IPv4 DNS IP for the device, only used with the static option above.

    +
    +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/templates/settings_power.html b/templates/settings_power.html new file mode 100644 index 0000000..7a73244 --- /dev/null +++ b/templates/settings_power.html @@ -0,0 +1,85 @@ +{% args config %} + + + + + + + OpenJBOD - Network + + + + + +
    + + + + + + + + +
    +
    +
    +

    Networking

    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +

    Note: After saving the settings, the device will need to restart to apply them. The board can be restarted on the Reset page.

    +
    +
    +
    +
    +

    Legend

    +
    +

    Ignore Power Switch - This causes the controller to ignore the power button input.

    +

    Turn on ATX on Boot - The controller will start the power supply as part of the boot process. This is mutually exclusive with Follow USB Power

    +

    Boot Delay - This is a timer in seconds that will elapse before the power supply is turned on.

    +

    Follow USB Power - The controller will start and stop the ATX power supply when it receives or stops receiving voltage from the microUSB port. This is mutually exclusive with Turn on ATX on Boot

    +

    Boot Delay - This is a timer in seconds that will elapse before the power supply is turned on.

    +
    +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/templates/settings_reset.html b/templates/settings_reset.html new file mode 100644 index 0000000..1b1e2aa --- /dev/null +++ b/templates/settings_reset.html @@ -0,0 +1,56 @@ + + + + + + + OpenJBOD - Network + + + + + + +
    + + + + + + + + +
    +
    +
    +

    Reset

    +
    +

    Reset RP2040

    +

    Warning: Performing this action will cause the controller to reset. The power supply will remain on, the OpenJBOD software and configuration will reload.

    + Reset RP2040 +

    Reset configuration

    +

    Warning: Performing this action will cause the entire configuration to restore to defaults and restart the RP2040 to apply the config. This includes users!

    + Reset configuration +
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/templates/settings_tls.html b/templates/settings_tls.html new file mode 100644 index 0000000..8dd5073 --- /dev/null +++ b/templates/settings_tls.html @@ -0,0 +1,135 @@ +{% args config %} + + + + + + + OpenJBOD - Network + + + + + +
    + + + + + + + + +
    +
    +
    +

    SSL/TLS

    +
    +
    +
    + + +
    + +

    Note: After saving the settings, the device will need to restart to apply them. The board can be restarted on the Reset page.

    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    Legend

    +
    +

    At this time, SSL is more unstable than non-SSL due to an exception. It will work, but you may experience occasional ECONNRESET errors.

    +

    Use SSL/TLS - This will start the webserver on port 443 with SSL/TLS enabled. If a valid certificate/certificate chain cannot be loaded, the webserver will revert to non-SSL on port 80.

    +

    Certificate - This must be a DER-formatted, Elliptic-curve certificate file.

    +

    Key - This must be a DER-formatted, Elliptic-curve key file.

    +

    You can generate a self-signed certificate using OpenSSL and the following commands:

    +
    openssl ecparam -name prime256v1 -genkey -noout -out key.der -outform DER
    +
    openssl req -new -x509 -key key.der -out cert.der -outform DER -days 365 -nodes
    +
    +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/templates/settings_users.html b/templates/settings_users.html new file mode 100644 index 0000000..3dda4a7 --- /dev/null +++ b/templates/settings_users.html @@ -0,0 +1,100 @@ +{% args config %} + + + + + + + OpenJBOD - Users + + + + + + +
    + + + + + + + + +
    +
    +
    +

    Users

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    User IDUsernameChange PasswordPassword
    1
    2
    3
    4
    5
    + +
    +
    +
    +
    +

    Legend

    +
    +

    User Table - Here you can change usernames and passwords. Passwords can only be changed with the 'Change Password' item checked. New users are created by setting a username and a password. Users may be deleted by clearing the username field. If you change the username of the user you are currently on, you will be forced to sign in again.

    +
    +
    +
    +
    +
    + + \ No newline at end of file