Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@

## What's new in this fork

### 2026-05-28 — New `time` parameter type

A new `time` parameter type shows a native time picker in the UI and passes the selected time to the script in a configurable format.

**Configuration example:**
```json
{
"name": "start_time",
"type": "time",
"time_format": "%H:%M"
}
```

- `time_format` is a Python [strftime](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) format string. Default: `%H:%M` (24-hour HH:MM).
- The UI always shows a native time picker. The script receives the time in the configured format.

### 2026-05-28 — HTTP security headers

All responses now include the following security headers:

| Header | Value |
|--------|-------|
| `X-Content-Type-Options` | `nosniff` |
| `Referrer-Policy` | `strict-origin-when-cross-origin` |
| `Content-Security-Policy` | `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' ws: wss:; frame-ancestors 'none'; object-src 'none'` |

`X-Frame-Options: DENY` was already present; `frame-ancestors 'none'` in the CSP provides equivalent coverage for modern browsers.

### 2025-05-27 — New `date` parameter type

A new `date` parameter type shows a native date picker in the UI and passes the selected date to the script in a configurable format.
Expand Down Expand Up @@ -52,7 +80,7 @@ No script modifications are needed - you configure each script in Script server
[Admin interface screenshots](https://github.com/bugy/script-server/wiki/Admin-interface)

## Features
- Different types of script parameters (text, integer, date, flag, dropdown, file upload, etc.)
- Different types of script parameters (text, integer, date, time, flag, dropdown, file upload, etc.)
- Real-time script output
- Users can send input during script execution
- Auth (optional): LDAP, Google OAuth, htpasswd file
Expand Down
15 changes: 15 additions & 0 deletions src/model/parameter_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def _setup(self):
self.excluded_files_matcher = _resolve_excluded_files(config, 'excluded_files', self._list_files_dir)

self.date_format = config.get('date_format', '%Y-%m-%d')
self.time_format = config.get('time_format', '%H:%M')
self.constant = read_bool_from_config('constant', config, default=False)

ui_config = config.get('ui')
Expand Down Expand Up @@ -325,6 +326,13 @@ def map_to_script(self, user_value):
except ValueError:
pass

if self.type == 'time' and isinstance(user_value, str) and user_value:
try:
time_obj = datetime.strptime(user_value, '%H:%M')
return time_obj.strftime(self.time_format)
except ValueError:
pass

if isinstance(user_value, list):
return [self._ui_value_mapper.map_to_script_value(single_value) for single_value in user_value]
else:
Expand Down Expand Up @@ -400,6 +408,13 @@ def validate_value(self, value_wrapper: ScriptValueWrapper, *, ignore_required=F
except ValueError:
return 'should be a valid date in YYYY-MM-DD format, but was ' + value_string

if self.type == 'time':
try:
datetime.strptime(user_value, '%H:%M')
return None
except ValueError:
return 'should be a valid time in HH:MM format, but was ' + value_string

if self.type in ('ip', 'ip4', 'ip6'):
try:
address = ip_address(user_value.strip())
Expand Down
9 changes: 5 additions & 4 deletions src/tests/auth/test_auth_keycloak_openid.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@

REALM_URL = 'http://my-keycloak.net/realms/master'

access_expiration_duration = 0.1
refresh_expiration_duration = 0.6
access_expiration_duration = 0.5
refresh_expiration_duration = 2.0
auth_info_ttl = 1.0


class OauthServerMock:
Expand Down Expand Up @@ -187,7 +188,7 @@ async def test_success_validate_after_refresh(self):

self.oauth_server.set_groups('bugy', ['g3'])

await gen.sleep(0.4 + 0.1)
await gen.sleep(auth_info_ttl + 0.5)

valid_1 = await self.authenticator.validate_user(username, mock_request_handler(previous_request=request_1))
self.assertTrue(valid_1)
Expand Down Expand Up @@ -296,7 +297,7 @@ def create_authenticator(self, dump_file=None):
'client_id': 'my-client',
'secret': 'top_secret',
'group_support': True,
'auth_info_ttl': 0.4,
'auth_info_ttl': auth_info_ttl,
'state_dump_file': dump_file
})

Expand Down
48 changes: 48 additions & 0 deletions src/tests/parameter_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,18 @@ def test_map_to_script_date_compact_format(self):
parameter_model = create_parameter_model('param1', type='date', date_format='%Y%m%d')
self.assertEqual('20240315', parameter_model.map_to_script('2024-03-15'))

def test_map_to_script_time_default_format(self):
parameter_model = create_parameter_model('param1', type='time')
self.assertEqual('14:30', parameter_model.map_to_script('14:30'))

def test_map_to_script_time_custom_format(self):
parameter_model = create_parameter_model('param1', type='time', time_format='%H%M')
self.assertEqual('1430', parameter_model.map_to_script('14:30'))

def test_map_to_script_time_with_seconds_format(self):
parameter_model = create_parameter_model('param1', type='time', time_format='%H:%M:%S')
self.assertEqual('14:30:00', parameter_model.map_to_script('14:30'))


class TestDefaultValue(unittest.TestCase):

Expand Down Expand Up @@ -714,6 +726,42 @@ def test_date_parameter_when_invalid_day(self):
error = validate_value(parameter, '2024-02-30')
self.assert_error(error)

def test_time_parameter_when_valid(self):
parameter = create_parameter_model('param', type='time')

error = validate_value(parameter, '14:30')
self.assertIsNone(error)

def test_time_parameter_when_midnight(self):
parameter = create_parameter_model('param', type='time')

error = validate_value(parameter, '00:00')
self.assertIsNone(error)

def test_time_parameter_when_end_of_day(self):
parameter = create_parameter_model('param', type='time')

error = validate_value(parameter, '23:59')
self.assertIsNone(error)

def test_time_parameter_when_invalid_format(self):
parameter = create_parameter_model('param', type='time')

error = validate_value(parameter, '14:30:00')
self.assert_error(error)

def test_time_parameter_when_not_a_time(self):
parameter = create_parameter_model('param', type='time')

error = validate_value(parameter, 'hello')
self.assert_error(error)

def test_time_parameter_when_invalid_hour(self):
parameter = create_parameter_model('param', type='time')

error = validate_value(parameter, '25:00')
self.assert_error(error)

def test_file_upload_parameter_when_valid(self):
parameter = create_parameter_model('param', type='file_upload')

Expand Down
10 changes: 7 additions & 3 deletions src/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ def create_script_param_config(
ui_separator_type=None,
ui_separator_title=None,
values_ui_mapping=None,
date_format=None):
date_format=None,
time_format=None):
method_params = dict(locals())
conf = {'name': param_name}

Expand Down Expand Up @@ -193,6 +194,7 @@ def create_script_param_config(
'stdin_expected_text': 'stdin_expected_text',
'values_ui_mapping': 'values_ui_mapping',
'date_format': 'date_format',
'time_format': 'time_format',
}

if values_script is not None:
Expand Down Expand Up @@ -314,7 +316,8 @@ def create_parameter_model(name=None,
ui_separator_type=None,
ui_separator_title=None,
values_ui_mapping=None,
date_format=None):
date_format=None,
time_format=None):
config = create_script_param_config(
name,
type=type,
Expand All @@ -341,7 +344,8 @@ def create_parameter_model(name=None,
ui_separator_type=ui_separator_type,
ui_separator_title=ui_separator_title,
values_ui_mapping=values_ui_mapping,
date_format=date_format)
date_format=date_format,
time_format=time_format)

if all_parameters is None:
all_parameters = []
Expand Down
20 changes: 18 additions & 2 deletions src/web/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,25 @@ def exception_to_code_and_message(exception):
return None, None


def _set_security_headers(handler):
handler.set_header('X-Frame-Options', 'DENY')
handler.set_header('X-Content-Type-Options', 'nosniff')
handler.set_header('Referrer-Policy', 'strict-origin-when-cross-origin')
handler.set_header(
'Content-Security-Policy',
"default-src 'self'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data:; "
"font-src 'self' data:; "
"connect-src 'self' ws: wss:; "
"frame-ancestors 'none'; "
"object-src 'none'"
)


class BaseRequestHandler(tornado.web.RequestHandler):
def set_default_headers(self):
self.set_header('X-Frame-Options', 'DENY')
_set_security_headers(self)

if self.application.server_config.xsrf_protection == XSRF_PROTECTION_TOKEN:
# This is needed to initialize cookie (by default tornado does it only on html template rendering)
Expand Down Expand Up @@ -119,7 +135,7 @@ def write_error(self, status_code, **kwargs):

class BaseStaticHandler(tornado.web.StaticFileHandler):
def set_default_headers(self):
self.set_header('X-Frame-Options', 'DENY')
_set_security_headers(self)


class GetServerConf(BaseRequestHandler):
Expand Down
Loading
Loading