Skip to content

Commit

Permalink
update password connection -> still need hijaker + proto behavior of …
Browse files Browse the repository at this point in the history
…golang http
  • Loading branch information
ElNiak committed Jan 3, 2024
1 parent 331d46f commit bd4fafd
Show file tree
Hide file tree
Showing 16 changed files with 247 additions and 168 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ make env; make install;
### PySSH3 server

```bash
./ssh3_env/bin/activate && python3 py-ssh3/server_cli.py --help
./ssh3_env/bin/activate && python3 py-ssh3/server_cli.py --generateSelfSignedCert --enablePasswordLogin --bind "127.0.0.1:4443" --urlPath "/my-secret-path" --verbose --insecure
./ssh3_env/bin/activate && sudo -E env PATH=$PATH python3 py-ssh3/server_cli.py --help
./ssh3_env/bin/activate && sudo -E env PATH=$PATH python3 py-ssh3/server_cli.py --generateSelfSignedCert --enablePasswordLogin --bind "127.0.0.1:4443" --urlPath "/my-secret-path" --verbose --insecure
```

#### Authorized keys and authorized identities
Expand Down Expand Up @@ -51,4 +51,5 @@ TODO
- [ ] Add more features
- [ ] Add threading support
- [ ] Inspire more from [paramiko]
- [ ] Secure version
- [ ] Secure version
- [ ] request.url.scheme == "ssh3"
14 changes: 8 additions & 6 deletions py-ssh3/agsi_ssh3.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

templates = Jinja2Templates(directory=os.path.join(ROOT, "asgi/templates"))

# TODO clean

async def homepage(request):
"""
Expand Down Expand Up @@ -128,13 +129,14 @@ async def wt(scope: Scope, receive: Receive, send: Send) -> None:


starlette = Starlette(
debug=True,
routes=[
Route("/", homepage),
Route("/{size:int}", padding),
Route("/echo", echo, methods=["POST"]),
Route("/logs", logs),
WebSocketRoute("/ws", ws),
Mount(STATIC_URL, StaticFiles(directory=STATIC_ROOT, html=True)),
# Route("/", homepage),
# Route("/{size:int}", padding),
# Route("/echo", echo, methods=["POST"]),
# Route("/logs", logs),
# WebSocketRoute("/ws", ws),
# Mount(STATIC_URL, StaticFiles(directory=STATIC_ROOT, html=True)),
]
)

Expand Down
3 changes: 2 additions & 1 deletion py-ssh3/auth/openid_connect.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from urllib import request
import webbrowser
import http.server
import socketserver
Expand All @@ -23,7 +24,7 @@ def connect(oidc_config: OIDCConfig, issuer_url: str, do_pkce: bool):
# Discover the provider
# Note: Discovery endpoint can vary by provider
discovery_url = f"{issuer_url}/.well-known/openid-configuration"
oidc_provider_config = requests.get(discovery_url).json()
oidc_provider_config = request.get(discovery_url).json()

authorization_endpoint = oidc_provider_config["authorization_endpoint"]
token_endpoint = oidc_provider_config["token_endpoint"]
Expand Down
20 changes: 14 additions & 6 deletions py-ssh3/client_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from ssh3.ssh3_client import *
from cryptography.hazmat.primitives import serialization
from ssh3.version import *
from auth.openid_connect import connect as oicd_connect
# from auth.openid_connect import connect as oicd_connect
from ssh3.conversation import *
import getpass
from ssh3.channel import *
Expand Down Expand Up @@ -289,7 +289,7 @@ async def main():
is_client=True,
max_datagram_frame_size=65536,
max_datagram_size=defaults.max_datagram_size,
quic_logger = QuicFileLogger("qlogs/"),
quic_logger = QuicFileLogger("qlogs_client/"),
secrets_log_file=secrets_log_file
)
configuration.verify_mode = ssl.CERT_REQUIRED if not args.insecure else ssl.CERT_NONE
Expand Down Expand Up @@ -321,6 +321,8 @@ async def main():

logging.debug(f"Dialing QUIC host at {hostname}:{port}")

conv = None # uninitialized conversation

async def establish_client_connection(client):
log.info(f"Establishing client connection with {client}")
if not client or client == -1:
Expand All @@ -334,11 +336,16 @@ async def establish_client_connection(client):
log.info(f"Conversation is {conv}")

# HTTP request over QUIC
# perform request
req = HttpRequest(method="CONNECT", url=URL(url_from_param))
# perform request
new_url = URL(url_from_param.replace("https","ssh3")) # TODO -> should replace Proto
log.info(f"New URL is {new_url}")
req = HttpRequest(method="CONNECT", url=new_url)
req.headers['user-agent'] = get_current_version()
# req.protocol = "ssh3"
req.headers['protocol'] = "ssh3" # TODO -> should replace Proto
log.info(f"Request is {req}")
# TODO seems not totally correct and secure
log.info(f"Request is {req}")

# await asyncio.gather(*coros)
# req.Proto = "ssh3" # TODO
# process http pushes
Expand Down Expand Up @@ -411,7 +418,8 @@ async def establish_client_connection(client):
elif isinstance(method, OIDCAuthMethod):
# Assuming an OIDC connection method
# TODO
token, err = oicd_connect(method.oidc_config(), method.oidc_config().issuer_url, method.do_pkce)
# token, err = oicd_connect(method.oidc_config(), method.oidc_config().issuer_url, method.do_pkce)
token, err = None, None
if err:
log.error(f"Could not get token: {err}")
else:
Expand Down
7 changes: 4 additions & 3 deletions py-ssh3/http3/http3_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,17 @@

class URL:
def __init__(self, url: str) -> None:
self.url = url
parsed = urlparse(url)

self.authority = parsed.netloc
self.full_path = parsed.path or "/"
if parsed.query:
self.full_path += "?" + parsed.query
self.scheme = parsed.scheme
logger.debug(f"URL initialized with authority: {self.authority}, full_path: {self.full_path}, scheme: {self.scheme}")


def __str__(self) -> str:
return self.url

class HttpRequest:
def __init__(
Expand All @@ -63,7 +65,6 @@ def __init__(
) -> None:
if headers is None:
headers = {}

self.content = content
self.headers = headers
self.method = method
Expand Down
5 changes: 3 additions & 2 deletions py-ssh3/http3/http3_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from aioquic.quic.events import DatagramFrameReceived, ProtocolNegotiated, QuicEvent
from aioquic.quic.logger import QuicFileLogger
from aioquic.tls import SessionTicket

import util.globals as glob

try:
import uvloop
Expand All @@ -34,6 +34,7 @@

log = logging.getLogger(__name__)


AsgiApplication = Callable
HttpConnection = Union[H0Connection, H3Connection]

Expand Down Expand Up @@ -437,7 +438,7 @@ def http_event_received(self, event: H3Event) -> None:
transmit=self.transmit,
)
self._handlers[event.stream_id] = handler
asyncio.ensure_future(handler.run_asgi(application))
asyncio.ensure_future(handler.run_asgi(glob.APPLICATION))
elif (
isinstance(event, (DataReceived, HeadersReceived))
and event.stream_id in self._handlers
Expand Down
175 changes: 85 additions & 90 deletions py-ssh3/linux_server/auth.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import base64
import logging
import util.globals as glob
from typing import Callable
from util.linux_util.linux_user import *
from aioquic.asyncio.server import QuicServer
from linux_server.handlers import *
from ssh3.version import *
from ssh3.conversation import *
from http3.http3_server import HttpServerProtocol
from aioquic.h3.connection import H3_ALPN, H3Connection
from aioquic.h3.events import (
Expand All @@ -16,13 +18,14 @@
)
from aioquic.quic.events import DatagramFrameReceived, ProtocolNegotiated, QuicEvent
from ssh3.version import *
from starlette.responses import PlainTextResponse, Response
from aioquic.tls import *

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

SERVER_NAME = get_current_version()


class AuthHttpServerProtocol(HttpServerProtocol):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
Expand All @@ -43,92 +46,84 @@ def quic_event_received(self, event: QuicEvent) -> None:



# async def handle_auths(
# enable_password_login: bool,
# default_max_packet_size: int,
# handler_func: callable,
# quic_server: QuicServer,
# ):
# """
# Handle different types of authentication for a given HTTP request.
# """
# # Set response server header
# # request_handler = quic_server._create_protocol.request_handler
# request_handler.send({
# "type": "http.response.start",
# "status": 200,
# "headers": [(b"server", b"MySSH3Server")] # Replace with your server version
# })

# # Check SSH3 version
# user_agent = request_handler.scope["headers"].get(b"user-agent", b"").decode()
# major, minor, patch = parse_version(user_agent) # Implement this function
# if major != MAJOR or minor != MINOR:
# request_handler.send({
# "type": "http.response.body",
# "body": b"Unsupported version",
# "more_body": False,
# })
# return

# # Check if connection is complete
# if not isinstance(request_handler.connection._quic, QuicConnection) or not request_handler.connection._quic._handshake_complete:
# request_handler.send({
# "type": "http.response.start",
# "status": 425, # HTTP StatusTooEarly
# "headers": []
# })
# return

# # Create a new conversation
# # Implement NewServerConversation based on your protocol's specifics
# conv = await NewServerConversation(
# request_handler.connection._quic,
# default_max_packet_size
# )

# # Handle authentication
# authorization = request_handler.scope["headers"].get(b"authorization", b"").decode()
# if enable_password_login and authorization.startswith("Basic "):
# await handle_basic_auth(handler_func, conv, request_handler)
# elif authorization.startswith("Bearer "):
# username = request_handler.scope["headers"].get(b":path").decode().split("?", 1)[0].lstrip("/")
# conv_id = base64.b64encode(conv.id).decode()
# await HandleBearerAuth(username, conv_id, handler_func, request_handler)
# else:
# request_handler.send({
# "type": "http.response.start",
# "status": 401,
# "headers": [(b"www-authenticate", b"Basic")]
# })

# await request_handler.transmit()

# async def handle_basic_auth(request, handler_func, conv, request_handler):
# # Extract Basic Auth credentials
# username, password, ok = extract_basic_auth(request)
# if not ok:
# return web.Response(status=401)

# # Replace this with your own authentication method
# ok = await user_password_authentication(username, password)
# if not ok:
# return web.Response(status=401)

# return await handler_func(username, conv, request)

# def extract_basic_auth(request):
# auth_header = request.headers.get('Authorization')
# if not auth_header:
# return None, None, False

# # Basic Auth Parsing
# try:
# auth_type, auth_info = auth_header.split(' ', 1)
# if auth_type.lower() != 'basic':
# return None, None, False

# username, password = base64.b64decode(auth_info).decode().split(':', 1)
# return username, password, True
# except Exception as e:
# return None, None, False
async def handle_auths(
request
):
"""
Handle different types of authentication for a given HTTP request.
enable_password_login: bool,
default_max_packet_size: int,
handler_func: callable,
quic_server: QuicServer
"""

# Set response server header
content = ""
status = 200
header = [(b"Server", SERVER_NAME)]

# Check SSH3 version
user_agent = b""
for h,v in request["headers"]:
if h == b"user-agent":
try:
user_agent = v.decode()
except InvalidSSHVersion:
logger.debug(f"Received invalid SSH version: {v}")
status = 400
return Response(content=b"Invalid version",
headers=header,
status_code=status)

major, minor, patch = parse_version(user_agent) # Implement this function
if major != MAJOR or minor != MINOR:
return Response(content=b"Unsupported version",
headers=header,
status_code=status)

protocols_keys = list(glob.QUIC_SERVER._protocols.keys())
tls_state = glob.QUIC_SERVER._protocols[protocols_keys[-1]]._quic.tls.state # TODO should be more modular, if if there is multiple protocols
logger.info(f"TLS state is {tls_state}")
# Check if connection is complete
if not tls_state == State.SERVER_POST_HANDSHAKE:
status = 425
return Response(content="",
headers=header,
status_code=status)

# Create a new conversation
# Implement NewServerConversation based on your protocol's specifics
conv = await new_server_conversation(
max_packet_size=glob.DEFAULT_MAX_PACKET_SIZE,
queue_size=10,
tls_state= tls_state
)
logger.info(f"Created new conversation {conv}")
# Handle authentication
authorization = b""
for h,v in request["headers"]:
if h == b"authorization":
try:
authorization = v.decode()
except Exception:
logger.debug(f"Received invalid authorization version: {v}")
status = 400
return Response(content=b"Invalid authorization",
headers=header,
status_code=status)
logger.info(f"Received authorization {authorization}")
if glob.ENABLE_PASSWORD_LOGIN and authorization.startswith("Basic "):
logger.info("Handling basic auth")
return await handle_basic_auth(request=request, conv=conv)
elif authorization.startswith("Bearer "):
logger.info("Handling bearer auth")
username = request.headers.get(b":path").decode().split("?", 1)[0].lstrip("/")
conv_id = base64.b64encode(conv.id).decode()
return await handle_bearer_auth(username, conv_id)
else:
logger.info("Handling no auth")
header.append((b"www-authenticate", b"Basic"))
status = 401
return Response(content=content,
headers=header,
status_code=status)
2 changes: 0 additions & 2 deletions py-ssh3/linux_server/authorized_identities.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend
from util.type import *
from auth.openid_connect import *

class Identity:
def verify(self, candidate, base64_conversation_id):
Expand Down Expand Up @@ -89,4 +88,3 @@ def parse_authorized_identities_file(user, file_path):
logging.error(f"Cannot parse identity line {line_number}: {str(e)}")
return identities

# Define classes or functions as needed, for example, JWTTokenString or verify_raw_token
Loading

0 comments on commit bd4fafd

Please sign in to comment.