Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add native websocket support #2350

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ The table below shows which release corresponds to each branch, and what date th
| [2.2.0](#220) | | Jan 5, 2015

## 4.13.0 (`dev`)

- [#2350][2350] add native websocket support
Copy link
Member

Choose a reason for hiding this comment

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

Please add new entries to the bottom of the list

- [#2242][2242] Term module revamp: activating special handling of terminal only when necessary
- [#2277][2277] elf: Resolve more relocations into GOT entries
- [#2281][2281] FIX: Getting right amount of data for search fix
Expand Down Expand Up @@ -99,6 +99,7 @@ The table below shows which release corresponds to each branch, and what date th
- [#2347][2347] Fix/workaround Unicorn Engine 1GB limit that calls exit()
- [#2233][2233] Fix gdb.debug: exe parameter now respected, allow empty argv

[2350]: https://github.com/Gallopsled/pwntools/pull/2350
[2242]: https://github.com/Gallopsled/pwntools/pull/2242
[2277]: https://github.com/Gallopsled/pwntools/pull/2277
[2281]: https://github.com/Gallopsled/pwntools/pull/2281
Expand Down
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ sphinx==1.8.6; python_version<'3'
sphinx>=4.5.0; python_version>='3'
sphinx_rtd_theme
sphinxcontrib-autoprogram<=0.1.5
websocket-client
13 changes: 13 additions & 0 deletions docs/source/tubes/wstube.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.. testsetup:: *

from pwn import *
from websocket import WebSocket, ABNF, WebSocketException, WebSocketTimeoutException
Copy link
Member

Choose a reason for hiding this comment

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

This isn't necessary since it's not used in the doc tests themselves


:mod:`pwnlib.tubes.wstube` --- Wstube
===========================================================

.. automodule:: pwnlib.tubes.wstube

.. autoclass:: pwnlib.tubes.wstube.wstube
:members:

1 change: 1 addition & 0 deletions pwn/toplevel.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from pwnlib.tubes.server import server
from pwnlib.tubes.ssh import ssh
from pwnlib.tubes.tube import tube
from pwnlib.tubes.wstube import wstube
from pwnlib.ui import *
from pwnlib.util import crc
from pwnlib.util import iters
Expand Down
3 changes: 2 additions & 1 deletion pwnlib/tubes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
from pwnlib.tubes import sock
from pwnlib.tubes import ssh
from pwnlib.tubes import tube
from pwnlib.tubes import wstube

__all__ = ['tube', 'sock', 'remote', 'listen', 'process', 'serialtube', 'server', 'ssh']
__all__ = ['tube', 'sock', 'remote', 'listen', 'process', 'serialtube', 'server', 'ssh', 'wstube']
129 changes: 129 additions & 0 deletions pwnlib/tubes/wstube.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@

# * Portions of this file are derived from pwntools-tube-websocket by frankli0324
# * under the MIT License.
# *
# * Copyright (c) frankli0324.
# * https://gist.github.com/frankli0324/795162a14be988a01e0efa0531f7ac5a
# *
# * Permission is hereby granted, free of charge, to any person obtaining a copy
# * of this software and associated documentation files (the "Software"), to deal
# * in the Software without restriction, including without limitation the rights
# * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# * copies of the Software, and to permit persons to whom the Software is
# * furnished to do so, subject to the following conditions:
# *
# * The above copyright notice and this permission notice shall be included in all
# * copies or substantial portions of the Software.
from __future__ import absolute_import
from __future__ import division
from websocket import WebSocket, ABNF, WebSocketException, WebSocketTimeoutException

from pwnlib.tubes.tube import tube


class wstube(tube):
"""
A basic websocket interface that wrapped as a tube.

Arguments:
url (str): The websocket server's URL to connect to.
headers (dict): The same headers as the websocket protocol.
Copy link
Member

Choose a reason for hiding this comment

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

Aren't those additional headers to pass around?
How could we test this?


Examples:

>>> ws = wstube('wss://echo.websocket.events')
Copy link
Member

Choose a reason for hiding this comment

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

Can we host a basic echo server ourselves in ci instead of relying on an external service? Those tend to be flakey and disturb the testing flow from time to time.

>>> ws.recv()
b'echo.websocket.events sponsored by Lob.com'
>>> for i in range(3):
... ws.send(b'test')
... ws.recv(2)
... ws.recv(2)
b'te'
b'st'
b'te'
b'st'
b'te'
b'st'
>>> ws.sendline(b'test')
>>> ws.recv()
b'test\\n'
>>> ws.send(b'12345asdfg')
>>> ws.recvregex(b'[0-9]{5}')
b'12345'
>>> ws.recv()
b'asdfg'
>>> ws.close()
"""
def __init__(self, url, headers=None, *args, **kwargs):
if headers is None:
headers = {}
super(wstube, self).__init__(*args, **kwargs)
self.closed = False
self.sock = WebSocket()
self.url = url
self.sock.connect(url, header=headers)
Copy link
Member

Choose a reason for hiding this comment

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

I think it is worth it to forward other useful arguments to the constructor like host, origin, cookie, and subprotocols. As well as the other ones.

Copy link
Member

Choose a reason for hiding this comment

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

Handling the proxy configured in context.proxy would be great too!



Copy link
Member

Choose a reason for hiding this comment

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

Maybe add a magic __call__ handler to forward calls to the underlying web socket instance like ping()

Some __getattr__ forward to e.g. get the optional close reason of the remote after the server closed the connection would be cool too.

def recv_raw(self, numb):
Copy link
Member

Choose a reason for hiding this comment

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

can_recv_raw needs to be implemented too to handle timeouts in the tube model.

if self.closed:
raise EOFError

while True:
try:
data = self.sock.recv()
if isinstance(data, str):
data = data.encode()
break
except WebSocketTimeoutException:
return None
except WebSocketException:
self.shutdown("recv")
raise EOFError

if not data:
self.shutdown()
raise EOFError('Recv Error')

return data

def send_raw(self, data):
if self.closed:
raise EOFError

try:
self.sock.send_binary(data)
except WebSocketException as e:
self.shutdown()
raise EOFError('Send Error')

def settimeout_raw(self, timeout):
if getattr(self, 'sock', None):
self.sock.settimeout(timeout)

def connected_raw(self, direction):
try:
self.sock.ping()
opcode, data = self.sock.recv_data(True)
return opcode == ABNF.OPCODE_PONG
except:
return False

def close(self):
Copy link
Member

Choose a reason for hiding this comment

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

It should be possible to specify the web socket close reason

if not getattr(self, 'sock', None):
return

self.closed = True

self.sock.close()
self.sock = None
self._close_msg()

def _close_msg(self):
self.info('Closed connection to %s', self.url)

def shutdown_raw(self, direction):
if self.closed:
return

self.closed = True
self.sock.shutdown()
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ dependencies = [
"pathlib2; python_version < '3.4'",
"unix-ar; python_version >= '3'",
"zstandard",
"websocket-client",
]

[project.urls]
Expand Down