Skip to content

feat(cloud_server): implement SSL secure websocket from semver2 into semver3 - #723

Open
Boss-1s wants to merge 34 commits into
TimMcCool:mainfrom
Boss-1s:semver3-secure-ws
Open

Boss-1s wants to merge 34 commits into
TimMcCool:mainfrom
Boss-1s:semver3-secure-ws

Conversation

@Boss-1s

@Boss-1s Boss-1s commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Resolves nothing

Changes

  • Implemented SSL secure ws server (fix cloud server #658) from semver2 into semver 3

  • New class _SaWebSockectServer BaseWebSocketServer in sa.eventhandlers._base to keep things pythonic and organized

  • You can now link a cloud

  • refactoring everything related cause why not

Tasks

Tests

First, start up an insecure server to ensure it works:

import scratchattach as sa
from scratchattach.cloud.cloud import CustomCloud
import logging
from rich.traceback import install

install(show_locals=True)

# Init Server & Cloud Events

server_ip = '127.0.0.1'
server_port = 8080
project_id="1321224718"

server = sa.init_cloud_server("127.0.0.1",
                              server_port,
                              length_limit=65536,
                              allow_non_numeric=True,
                              whitelisted_projects=[project_id],
                              allow_nonscratch_names=False,
                              blocked_ips=["127.0.0.2"],
                              sync_players=True,
                              log_var_sets=True
                              )
server.start()


banned_cloud = CustomCloud(project_id='1234567',
                    cloud_host=f"ws://{server_ip}:{server_port}",
                    username = "Boss_1s",
                    length_limit = None,
                    allow_non_numeric = True,
                    _session = None,
                    header = None,
                    cookie = None,
                    origin = None,
                    print_connect_messages = True
                    )

bad_user_name_cloud = CustomCloud(project_id=project_id,
                    cloud_host=f"ws://{server_ip}:{server_port}",
                    username = "Boss_asdfghjkl",
                    length_limit = None,
                    allow_non_numeric = True,
                    _session = None,
                    header = None,
                    cookie = None,
                    origin = None,
                    print_connect_messages = True
                    )

cloud = CustomCloud(project_id=project_id,
                    cloud_host=f"ws://{server_ip}:{server_port}",
                    username = "Boss_1s",
                    length_limit = None,
                    allow_non_numeric = True,
                    _session = None,
                    header = None,
                    cookie = None,
                    origin = None,
                    print_connect_messages = True
                    )

no_user_cloud = CustomCloud(project_id=project_id,
                    cloud_host=f"ws://{server_ip}:{server_port}",
                    username = None,
                    length_limit = None,
                    allow_non_numeric = True,
                    _session = None,
                    header = None,
                    cookie = None,
                    origin = None,
                    print_connect_messages = True
                    )

no_project_cloud = CustomCloud(project_id=None,
                    cloud_host=f"ws://{server_ip}:{server_port}",
                    username="Boss_1s",
                    length_limit = None,
                    allow_non_numeric = True,
                    _session = None,
                    header = None,
                    cookie = None,
                    origin = None,
                    print_connect_messages = True
                    )

banned_cloud_events=banned_cloud.events()
bad_user_name_cloud_events=bad_user_name_cloud.events()
cloud_events=cloud.events()
no_user_cloud_events=no_user_cloud.events()
no_project_cloud_events=no_project_cloud.events()

While the above script is running, run websocat ws://127.0.0.1:8080 to interact with the websocket.

Once you have determined that this works, replace the above script with the one below:

import scratchattach as sa
from scratchattach.cloud.cloud import CustomCloud
import logging
from rich.traceback import install

install(show_locals=True)

# Init Server & Cloud Events

server_ip = '127.0.0.1'
server_port = 8080
project_id="1321224718"

# Test with ssl and make sure it works

server = sa.init_ssl_cloud_server("127.0.0.1",
                              server_port,
                              length_limit=65536,
                              allow_non_numeric=True,
                              whitelisted_projects=[project_id],
                              allow_nonscratch_names=False,
                              blocked_ips=["127.0.0.2"],
                              sync_players=True,
                              log_var_sets=True,
                              certfile="certfile.pem",
                              keyfile="keyfile.pem"
                              )
server.start()


banned_cloud = CustomCloud(project_id='1234567',
                    cloud_host=f"wss://{server_ip}:{server_port}",
                    username = "Boss_1s",
                    length_limit = None,
                    allow_non_numeric = True,
                    _session = None,
                    header = None,
                    cookie = None,
                    origin = None,
                    print_connect_messages = True
                    )

bad_user_name_cloud = CustomCloud(project_id=project_id,
                    cloud_host=f"wss://{server_ip}:{server_port}",
                    username = "Boss_asdfghjkl",
                    length_limit = None,
                    allow_non_numeric = True,
                    _session = None,
                    header = None,
                    cookie = None,
                    origin = None,
                    print_connect_messages = True
                    )

cloud = CustomCloud(project_id=project_id,
                    cloud_host=f"wss://{server_ip}:{server_port}",
                    username = "Boss_1s",
                    length_limit = None,
                    allow_non_numeric = True,
                    _session = None,
                    header = None,
                    cookie = None,
                    origin = None,
                    print_connect_messages = True
                    )

no_user_cloud = CustomCloud(project_id=project_id,
                    cloud_host=f"wss://{server_ip}:{server_port}",
                    username = None,
                    length_limit = None,
                    allow_non_numeric = True,
                    _session = None,
                    header = None,
                    cookie = None,
                    origin = None,
                    print_connect_messages = True
                    )

no_project_cloud = CustomCloud(project_id=None,
                    cloud_host=f"wss://{server_ip}:{server_port}",
                    username="Boss_1s",
                    length_limit = None,
                    allow_non_numeric = True,
                    _session = None,
                    header = None,
                    cookie = None,
                    origin = None,
                    print_connect_messages = True
                    )

banned_cloud_events=banned_cloud.events()
bad_user_name_cloud_events=bad_user_name_cloud.events()
cloud_events=cloud.events()
no_user_cloud_events=no_user_cloud.events()
no_project_cloud_events=no_project_cloud.events()

Then, generate an OpenSSL certificate:

openssl req -x509 -newkey rsa:4096 -keyout keyfile.pem -out certfile.pem -days 365 -nodes

Finally, run the server script, and interact with the WebSocket with websocat -k wss://127.0.0.1:8080

Notes

might wanna have #722 fixed before merging this, idk

…semver3

Signed-off-by: GitHub <noreply@github.com>
Signed-off-by: GitHub <noreply@github.com>
- Moved `BaseCloudServer` (Formerly `_SaCloudServer`) to sa.eventhandlers._base

Signed-off-by: GitHub <noreply@github.com>
Signed-off-by: GitHub <noreply@github.com>
@Boss-1s

Boss-1s commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor Author

@TheCommCraft

Comment thread scratchattach/eventhandlers/_base.py Resolved
Comment thread scratchattach/eventhandlers/_base.py Resolved
Comment thread scratchattach/eventhandlers/_base.py Resolved
Comment thread scratchattach/__init__.py Resolved
@TheCommCraft

Copy link
Copy Markdown
Collaborator

Tell me if you require help in applying these changes

(cherry picked from commit 8d30697)
Signed-off-by: GitHub <noreply@github.com>
### sa.eventhandlers._base

- changed list comp to generator comp in set_project_vars()  and set_var()
- reimplemented attribute type hints into BaseCloudServer. since type hints are inhierted, there is no need to restate them in child classes of BaseCloudServer.

### sa.eventhandlers.cloud_server

- added type hitns to __init__ of BaseCloudServer child classes
- added type hints to init_cloud_server and init_ssl_cloud_server
- revert changing warnings.warn to print in 9863d50

Signed-off-by: GitHub <noreply@github.com>
Signed-off-by: GitHub <noreply@github.com>
Signed-off-by: GitHub <noreply@github.com>
Signed-off-by: GitHub <noreply@github.com>
Comment thread scratchattach/eventhandlers/_base.py Resolved
Comment thread scratchattach/eventhandlers/_base.py Resolved
Co-authored-by: TheCommCraft <79996518+TheCommCraft@users.noreply.github.com>
Signed-off-by: Boss_1s <95505913+Boss-1s@users.noreply.github.com>
Signed-off-by: Boss_1s <95505913+Boss-1s@users.noreply.github.com>
@TheCommCraft

Copy link
Copy Markdown
Collaborator

I think that the custom cloud solution you added might cause extreme lag, it would be better to just not add a cloud at first (or you could create a cloud that just translates method calls of the cloud into those of the server)

@Boss-1s

This comment was marked as resolved.

Signed-off-by: GitHub <noreply@github.com>
@Boss-1s

This comment was marked as resolved.

@Boss-1s

Boss-1s commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

The only thing left is the set var prefix option, then we should be done here

@TheCommCraft

Copy link
Copy Markdown
Collaborator

Do you want to complete this PR or should I continue?

@Boss-1s

Boss-1s commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

jtlyk I am unable to resolve the conflicts; they are too complex

Signed-off-by: TheCommCraft <79996518+TheCommCraft@users.noreply.github.com>
@TheCommCraft
TheCommCraft requested a review from faretek1 November 6, 2026 17:43

@faretek1 faretek1 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is difficult for me to provide a useful review because I haven't interacted the codebase that much recently, but I have tried to provide a few comments. I do not see anything obviously wrong, however my judgement is limited

Some additional notes:

  • We are using a lot of Any and then converting to str. Maybe it is not possible now but it would probably be better to do that conversion earlier, so we do not get Any propagating everywhere. Unless if it would be a breaking change to a user-facing API
  • We are using SimpleWebSocketServer a lot although we really should change that (#734)
  • In truth, AI code review could probably be useful here

Comment thread scratchattach/eventhandlers/_base.py Outdated
Comment on lines +135 to +138
certfile: str|None = None,
keyfile: str|None = None,
ssl_version: int = ssl.PROTOCOL_TLSv1_2,
ssl_context: ssl.SSLContext|None = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These attrs should be specific to SSL servers right?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ie don't put them in the base class



class BaseCloud(AnyCloud[Union[str, int]]):
class BaseCloud(AnyCloud[Union[str, int, float]]):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small thing, but str | int | float is more concise

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would apply in a lot of cases in the codebase, e.g. every use of Optional, so this is more of a separate style issue


Args:
variable (str): The name of the cloud variable that should be set (provided without the cloud emoji)
value (Any): The value the cloud variable should be set to

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any is the wrong type here. should be str | int | float

"IP address or domain name of the host to bind the server to."
port: int
"Port to bind the server to."
tw_clients: dict[tuple[str, int], dict[str, Any]]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would a TypedDict be possible here instead of using Any?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better, but we could also just use a class.

"Port to bind the server to."
tw_clients: dict[tuple[str, int], dict[str, Any]]
"Dictionary containing information on connected clients."
tw_variables: dict[str, dict[str, Any]]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would a TypedDict be possible here instead of using Any?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to use str | int | float

Comment thread scratchattach/eventhandlers/_base.py Resolved
)
for varname in data
]
json_string = "\n".join(json.dumps(packet) for packet in packets)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't really a json string, but rather a number of lines of json (JSONL). Is this expected and is the scratchattach client also able to handle this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the intended way to transmit multiple packets in scratch cloud variable websockets

Comment on lines +691 to +692
intelligent_waits: bool = True,
max_retries: int = 2,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these arguments are unused. if this is intentional, they should also be removed from the docstring

Comment on lines +704 to +705
if self.disconnected.is_set():
return

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be more appropriate to raise an error if you are trying to set variables if you are disconnected?

Also, it is noteworthy that self.disconnected.is_set() is being used for the set_var methods but not for the get_var methods. Is this intentional? Perhaps it is useful to still be able to see the variables after you are disconnected?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that was the thought behind it

Comment on lines +738 to +740
while (progress < amount if amount >= 0 else progress == 0) and (
time.time() < end_time if end_time is not None else True
):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a pretty confusing condition

This branch was successfully deployed

1 active deployment
release — d78f40db Deployed Nov 6, 2026 by TheCommCraft via tests #785
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants