Skip to content

connect()

Open and return a ready Connection (blocking).

Four transport paths
  • message server: mshost set → resolve the least-loaded app server via MessageServerClient.resolve(group), then direct-TCP connect (TRANS-03).
  • SAProuter: saprouter set → prepend the NI_ROUTE prefix before the direct-TCP handshake (TRANS-02).
  • wRFC (WebSocket RFC over TLS): wshost set → route through connect_ws (SEC-05, D-16/D-17). wsport defaults to 443 and ws_path to /sap/bc/rfc (D-19). Optional ws_proxy_* params tunnel the connection through an HTTP CONNECT forward proxy (D-20).
  • SNC (Secure Network Communications): snc_lib set (and wshost absent) → wrap the direct-TCP transport in an :class:~saprfclib.snc.SncTransport that drives the GSS-API handshake to COMPLETE before any data is sent (SEC-02/03/04/06, D-13). snc_lib presence is the activation switch — there is no separate mode flag. snc_qop defaults to 3 (privacy) and snc_sso to False (D-12). wshost takes precedence: SNC-over-wRFC is out of scope for Phase 7.
  • direct: port = 3300 + int(sysnr) (gateway port), connect_tcp, handshake.

lang is the logon language. Accepts the one-character SAP code ('E' English, 'D' German, 'S' Spanish, …) or the two-character ISO code ('EN', 'DE', 'ES'); an ISO code is converted before the logon frame is built, matching the SDK's LANG option.

user and passwd may both be omitted. That is read as a deliberate anonymous attempt and the logon frame goes out without the user and password records — some systems answer a small set of function modules that way, while a hardened one refuses below the RFC layer and raises CommunicationError. Supplying exactly one of the two raises ValueError, since that is a missing setting rather than a request to connect anonymously. SNC connections are unaffected: snc_lib carries its own credentials.

strict_params controls what call() does with a keyword argument the function interface does not declare. The default (False) drops it and logs a warning, which is what callers porting from pyrfc expect when they pass a superset of kwargs across differing SAP releases. Set True to raise ValueError instead — worth doing when a dropped argument would change the result, since the server has no way to tell you an argument never arrived.

trace attaches an :class:~saprfclib.trace.RfcTrace, which writes an SDK-format trace file of every frame. It is a parameter rather than an environment variable on purpose: the SDK reads RFC_TRACE from the environment, but a process that starts writing traffic to disk because of a variable it inherited is a surprise, and the file — though credential-redacted — still contains everything else that crossed the wire. Turning it on should be visible at the call site.

The SAProuter and message-server wire formats were live-verified after this docstring first called them unverified: the NI_ROUTE payload is byte-exact against a capture (tests/golden/router/ni_route_payload.bin), a router that accepts a route answers NI_PONG and one that refuses answers NI_RTERR, and the message server answers the binary attach and server-list frames as MSG_SERVER. What remains unconfirmed is narrower and sits in router.py: some field boundaries inside a server-list entry, and whether the entry count is carried in the header or only implied by the payload length. passwd, ws_proxy_pass, snc_lib, snc_partnername and snc_myname are never logged or echoed into any log message or exception string (threats T-03-CRED2 / T-07-CRED / T-07-PROXY-CRED).

Source code in src/saprfclib/connection.py
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
def connect(
    ashost: str,
    sysnr: str | int,
    client: str,
    user: str | None = None,
    passwd: str | None = None,
    *,
    lang: str = _DEFAULT_LANG,
    strict_params: bool = False,
    timeout: float | None = None,
    connect_timeout: float | None = DEFAULT_CONNECT_TIMEOUT,
    read_timeout: float | None = DEFAULT_READ_TIMEOUT,
    metadata_cache: MetadataCache | None = None,
    metadata_cache_key: str | None = None,
    saprouter: str | None = None,
    mshost: str | None = None,
    msserv: int | str | None = None,
    ms_http_port: int | str | None = None,
    ms_use_http: bool = True,
    sysid: str | None = None,
    group: str | None = None,
    wshost: str | None = None,
    wsport: int | None = None,
    ws_path: str | None = None,
    ws_proxy_host: str | None = None,
    ws_proxy_port: int | None = None,
    ws_proxy_user: str | None = None,
    ws_proxy_pass: str | None = None,
    ws_tls_verify: bool = True,
    trace: RfcTrace | None = None,
    snc_lib: str | None = None,
    snc_partnername: str | None = None,
    snc_myname: str | None = None,
    snc_qop: int | None = None,
    snc_sso: bool | None = None,
    max_retries: int = 3,
    retry_delay: float = 1.0,
    tid_store: TidStore | None = None,
    unit_store: UnitStore | None = None,
) -> Connection:
    """Open and return a ready Connection (blocking).

    Four transport paths:
      - message server: ``mshost`` set → resolve the least-loaded app server via
        MessageServerClient.resolve(group), then direct-TCP connect (TRANS-03).
      - SAProuter: ``saprouter`` set → prepend the NI_ROUTE prefix before the
        direct-TCP handshake (TRANS-02).
      - wRFC (WebSocket RFC over TLS): ``wshost`` set → route through
        ``connect_ws`` (SEC-05, D-16/D-17). ``wsport`` defaults to 443 and
        ``ws_path`` to ``/sap/bc/rfc`` (D-19). Optional ``ws_proxy_*`` params
        tunnel the connection through an HTTP CONNECT forward proxy (D-20).
      - SNC (Secure Network Communications): ``snc_lib`` set (and ``wshost``
        absent) → wrap the direct-TCP transport in an :class:`~saprfclib.snc.SncTransport`
        that drives the GSS-API handshake to COMPLETE before any data is sent
        (SEC-02/03/04/06, D-13). ``snc_lib`` presence is the activation switch —
        there is no separate mode flag. ``snc_qop`` defaults to 3 (privacy) and
        ``snc_sso`` to False (D-12). ``wshost`` takes precedence: SNC-over-wRFC
        is out of scope for Phase 7.
      - direct: ``port = 3300 + int(sysnr)`` (gateway port), connect_tcp, handshake.

    ``lang`` is the logon language. Accepts the one-character SAP code ('E' English,
    'D' German, 'S' Spanish, …) or the two-character ISO code ('EN', 'DE', 'ES'); an
    ISO code is converted before the logon frame is built, matching the SDK's LANG
    option.

    ``user`` and ``passwd`` may both be omitted. That is read as a deliberate
    anonymous attempt and the logon frame goes out without the user and password
    records — some systems answer a small set of function modules that way, while a
    hardened one refuses below the RFC layer and raises ``CommunicationError``.
    Supplying exactly one of the two raises ``ValueError``, since that is a missing
    setting rather than a request to connect anonymously. SNC connections are
    unaffected: ``snc_lib`` carries its own credentials.

    ``strict_params`` controls what ``call()`` does with a keyword argument the
    function interface does not declare. The default (False) drops it and logs a
    warning, which is what callers porting from pyrfc expect when they pass a
    superset of kwargs across differing SAP releases. Set True to raise ValueError
    instead — worth doing when a dropped argument would change the result, since the
    server has no way to tell you an argument never arrived.

    ``trace`` attaches an :class:`~saprfclib.trace.RfcTrace`, which writes an
    SDK-format trace file of every frame. It is a parameter rather than an
    environment variable on purpose: the SDK reads ``RFC_TRACE`` from the
    environment, but a process that starts writing traffic to disk because of a
    variable it inherited is a surprise, and the file — though credential-redacted
    — still contains everything else that crossed the wire. Turning it on should
    be visible at the call site.

    The SAProuter and message-server wire formats were live-verified after this
    docstring first called them unverified: the NI_ROUTE payload is byte-exact
    against a capture (``tests/golden/router/ni_route_payload.bin``), a router
    that accepts a route answers ``NI_PONG`` and one that refuses answers
    ``NI_RTERR``, and the message server answers the binary attach and
    server-list frames as ``MSG_SERVER``. What remains unconfirmed is narrower and
    sits in ``router.py``: some field boundaries inside a server-list entry, and
    whether the entry count is carried in the header or only implied by the
    payload length. ``passwd``,
    ``ws_proxy_pass``, ``snc_lib``, ``snc_partnername`` and ``snc_myname`` are
    never logged or echoed into any log message or exception string (threats
    T-03-CRED2 / T-07-CRED / T-07-PROXY-CRED).
    """
    # Imported lazily so the direct-TCP facade carries no hard dependency on the
    # alternate-transport layer (router.py, plan 03-03 Task 2).
    from saprfclib.router import (
        open_route,
        open_route_async,
        parse_route_string,
    )

    user, passwd = _resolve_credentials(user, passwd, snc_lib=snc_lib, ashost=ashost)

    if mshost is not None:
        # Message-server group logon: resolve to a concrete (ashost, sysnr).
        ashost, sysnr = _resolve_via_message_server(
            mshost,
            group=group,
            sysid=sysid,
            msserv=msserv,
            ms_http_port=ms_http_port,
            use_http=ms_use_http,
            timeout=timeout,
            connect_timeout=connect_timeout,
            read_timeout=read_timeout,
        )

    # Gateway port. Confirmed by SAP's "TCP/IP Ports of All SAP Products":
    #   Gateway          sapgw<NN>    3300   range 3300-3399   33<NN>
    #   Gateway secured  sapgw<NN>s   4800   range 4800-4899   48<NN>
    # <NN> is the application server's own instance number here, unlike the
    # message server. Also confirmed live: the A4H message server reports
    # RFC=3300 and RFCS=4800 for a sysnr-00 application server.
    sysnr = _validate_sysnr(sysnr)
    port = (4800 if snc_lib is not None else 3300) + sysnr

    # ------------------------------------------------------------------ #
    # Transport routing (Phase 7): wRFC first, then SNC, then plain TCP.  #
    # Both branches are additive — when ``wshost`` and ``snc_lib`` are    #
    # None the plain connect_tcp path below is byte-for-byte unchanged    #
    # (SEC-01 / T-07-REGRESSION). ``wshost`` wins over ``snc_lib`` because #
    # SNC-over-wRFC is out of scope for Phase 7 (D-13).                    #
    # ------------------------------------------------------------------ #
    if wshost is not None:
        # wRFC over TLS (SEC-05, D-16/D-17). Lazy import mirrors the
        # router lazy import above so a bare ``import saprfclib`` never hard-
        # depends on the WebSocket stack at import time.
        from saprfclib.ws import connect_ws

        transport = connect_ws(
            wshost,
            wsport or 443,
            ws_path=ws_path or "/sap/bc/rfc?sap-apc-stateful=true",
            ws_proxy_host=ws_proxy_host,
            ws_proxy_port=ws_proxy_port,
            ws_proxy_user=ws_proxy_user,
            ws_proxy_pass=ws_proxy_pass,
            user=user,
            passwd=passwd,
            sap_client=client,
            verify=ws_tls_verify,
            timeout=timeout,
            read_timeout=read_timeout,
        )
        conn = Connection(
            transport,  # type: ignore[arg-type]
            strict_params=strict_params,
            metadata_cache=metadata_cache,
            metadata_cache_key=metadata_cache_key,
        )
    elif snc_lib is not None:
        # SNC (SEC-02/03/04/06, D-13): SAP protocol order requires the NI
        # version exchange to complete on the plain channel BEFORE the GSS
        # frames are sent. SncTransport then drives FR_INIT/FR_ACCEPT to
        # COMPLETE; GW connect and logon flow through the encrypted channel.
        #
        # T-07-CRED: snc_lib / snc_partnername / snc_myname are passed
        # straight through — never placed into a log or an exception string.
        from saprfclib.snc import SncTransport

        _inner = connect_tcp(
            ashost,
            port,
            timeout=timeout,
            connect_timeout=connect_timeout,
            read_timeout=read_timeout,
            trace=trace,
        )

        # Step 1: NI version exchange on the plain inner transport.
        _snc_sess = Session()
        try:
            _snc_lip = _inner._sock.getsockname()[0]
        except Exception:
            _snc_lip = "127.0.0.1"
        _inner.send_message(_snc_sess.start(local_ip=_snc_lip))
        _snc_sess.feed(_inner.recv_message())
        # _snc_sess is now NI_VERSIONED.

        # Step 2: GSS handshake on the versioned channel.
        transport = SncTransport(  # type: ignore[assignment]
            _inner,
            snc_lib=snc_lib,
            snc_partnername=snc_partnername,  # type: ignore[arg-type]
            snc_myname=snc_myname,
            snc_qop=snc_qop or 3,  # D-12: privacy is the default QOP
            snc_sso=snc_sso or False,  # D-12: SSO2 off by default (D-23 gap)
        )

        # Step 3: Connection with the pre-versioned session so _handshake()
        # resumes from NI_VERSIONED (skips the NI leg, starts at GW connect).
        conn = Connection(
            transport,  # type: ignore[arg-type]
            strict_params=strict_params,
            metadata_cache=metadata_cache,
            metadata_cache_key=metadata_cache_key,
        )
        conn._session = _snc_sess
        conn._snc_mode = True
    else:
        # ------------------------------------------------------------------ #
        # Classic async-core path (D-06/D-07): direct TCP / SAProuter /       #
        # message-server connections all use AsyncConnection + _LoopThread.   #
        # SAProuter NI_ROUTE is prepended inside the async setup coroutine.   #
        # Returns early — the shared saprouter/handshake lines below are for  #
        # SNC / wRFC paths only (scope boundary — Phase 9).                   #
        # ------------------------------------------------------------------ #
        loop_thread = _LoopThread()

        # Capture locals for the async closure (avoid late-binding issues).
        _ashost = ashost
        _port = port
        _timeout = timeout
        _connect_timeout = connect_timeout
        _read_timeout = read_timeout
        _metadata_cache = metadata_cache
        _metadata_cache_key = metadata_cache_key
        _saprouter = saprouter
        _client = client
        _user = user
        _passwd = passwd
        _lang = lang
        _strict = strict_params
        _sysnr = int(sysnr)
        _max_retries = max_retries
        _retry_delay = retry_delay
        _tid_store = tid_store
        _unit_store = unit_store

        async def _async_setup() -> AsyncConnection:
            # Use connect_tcp (sync, patchable in tests) wrapped in a thin async shim.
            # connect_async() uses real asyncio open_connection for non-blocking I/O.
            # This keeps the existing test suite (which patches connect_tcp) green (D-07).
            sync_t = connect_tcp(
                _ashost,
                _port,
                timeout=_timeout,
                connect_timeout=_connect_timeout,
                read_timeout=_read_timeout,
                trace=trace,
            )
            at: _SyncToAsyncTransport = _SyncToAsyncTransport(sync_t)
            if _saprouter is not None:
                hops = parse_route_string(_saprouter)
                await open_route_async(at, hops, _ashost, str(_port))
            ac = AsyncConnection(
                at,  # type: ignore[arg-type]
                max_retries=_max_retries,
                retry_delay=_retry_delay,
                tid_store=_tid_store,
                unit_store=_unit_store,
                strict_params=_strict,
                metadata_cache=_metadata_cache,
                metadata_cache_key=_metadata_cache_key,
            )
            await ac._handshake(
                client=_client,
                user=_user,
                passwd=_passwd,
                ashost=_ashost,
                sysnr=_sysnr,
                lang=_lang,
            )
            return ac

        try:
            async_conn = loop_thread.run(_async_setup())
        except Exception:
            loop_thread.close()
            raise
        return Connection._from_async(async_conn, loop_thread)

    if saprouter is not None:
        # Prepend the NI_ROUTE control frame before the handshake (TRANS-02).
        # Wire format confirmed from live capture 2026-06-27.
        # NOTE: only reached by SNC/wRFC branches (classic path returns above).
        hops = parse_route_string(saprouter)
        open_route(transport, hops, ashost, str(port))

    conn._handshake(
        client=client, user=user, passwd=passwd, ashost=ashost, sysnr=int(sysnr), lang=lang
    )
    return conn