Project

General

Profile

Web Client Setup

This chapter documents how to configure and setup the FWD Server to allow access to the FWD Clients via a web browser (a different client than the Swing Applet). The Web Clients were implemented to allow access to a ChUI/GUI FWD Client running remotely on another machine (note that at the time of this writing only ChUI clients - using the Console Driver - can be accessed via a browser). Once all setup is performed, when accessing the remote FWD Client via the web browser, the user will have the same experience as if the user has connected to the remote machine and started the FWD Client there, manually: this means that the user will have the same OS security permissions and will be allowed to start processes, read files, access a shell to that machine, etc. But, an inherent limitation for the web clients is that they can start only processes which remain active in the same console window as the application client.

TBD: Add discussion for Spawner Setup and Configuration.

Broker startup, registration, multi-broker deployments and broker selection are documented in the Remote Launchers chapter. Reverse proxy configuration for spawned web clients is documented in the Remote Web Client Spawning via Brokers section below.

System Requirements

The FWD Web Client should be able to run on any web browser which supports HTML5, Web Sockets and AJAX requests. At the time of this writing, the web client is expected to work properly with the latest versions of the Google Chrome, Mozilla Firefox, Internet Explorer and Safari browsers; from these, testing showed that the most reliable (in terms of performance and stability) is Google Chrome.

Considering how the dedicated web server (ran by each remote FWD Client, see the Under the Hood section, for more details) computes its dedicated port, it is best for the machine on which the remote FWD Client is launched to be directly accessible from the machine used by the user to start the Web Client (i.e. no intermediate NAT-enabled router is required to access the FWD application server). If this is not set then the embedded web server (upon which the web client depends) will not be started at server startup. No error messages will be written to the log. The core idea is that there is no support for insecure web protocols.

The last requirement is that an user allowed to connect to the FWD application server via a web browser needs to have access to valid OS credentials on the remote machine on which the remote FWD Client is started (which at the time of this writing is the same machine on which the FWD application server is running). Without valid credentials, the user will not be allowed to pass the initial login page.

Setup

TBD: Needs updates to document the full range of current configuration and options.

To access web clients, it is required for the FWD server to have the spawner tool configured properly - as the spawner is not used only for launching web clients, details about its setup can be found in the Application Server Installation chapter of this book. This chapter assumes the client spawner tool is setup properly.

In addition to the spawner, the FWD server itself must be configured with a secure port. This is defined in the bootstrap configuration or is passed as a bootstrap configuration override on the server driver command line. The values that must be set include net:connection:secure=true and net:server:secure_port=<port>. For more details on this, please see Part 2: Bootstrap Configuration.

The remaining configuration is all related to the FWD server directory, which instructs how the terminal will be displayed in the web browser. The directory node is named chuiWeb and can reside either in /server/default/ or the /server/<server-id>/ section, with the later having priority over the default configuration; also, if a chuiWeb/<node>/ node is found in both per-server and default configuration, the per-server value will be used. The following table describe all the possible configurations (all nodes are children of the chuiWeb/ node), their default values and meaning.

chuiWeb/ Node Type Default Value Mandatory Description
enabled boolean false yes Flag identifying if this application server is configured for launching web clients.
port integer 0 no An explicit port to start the FWD Client's embedded web server, or 0 to let the OS assign one. This applies when no ports range is configured: a non-zero value is used as the explicit embedded web server port, while a value of 0 means the new web client's port is assigned by the OS.
fontsize integer 12 no The font's point size used for all text.
rows integer 24 no The number of rows of text on the terminal screen.
columns integer 80 no The number of columns of text on the terminal screen.
webSocketTimeout integer -1 no Web socket timeout (in milliseconds). This is a timeout after which the web socket is closed.
watchdogTimeout integer -1 no Watchdog timeout (in milliseconds). After the web socket is closed, the watchdog will wait at least that certain amount of time and, if the web socket was connected again, it will decide to terminate the FWD client, as the remote user is no longer connected to it. A value of -1 will mean the remote FWD client will be terminated as soon as the web socket is closed.

Considering the table above, the minimal chuiWeb/ node looks like:

<node class="container" name="chuiWeb">
   <node class="boolean" name="enabled">
      <node-attribute name="value" value="TRUE"/>
   </node>
</node>

while the full node looks like:

<node class="container" name="chuiWeb">
   <node class="boolean" name="enabled">
      <node-attribute name="value" value="TRUE" />
   </node>
   <node class="string" name="background">
      <node-attribute name="value" value="0x000000" />
   </node>
   <node class="string" name="foreground">
      <node-attribute name="value" value="0xFFA500" />
   </node>
   <node class="string" name="selection">
      <node-attribute name="value" value="0x0000FF" />
   </node>
   <node class="string" name="fontname">
      <node-attribute name="value" value="monospaced" />
   </node>
   <node class="integer" name="fontsize">
      <node-attribute name="value" value="12" />
   </node>
   <node class="integer" name="rows">
      <node-attribute name="value" value="24" />
   </node>
   <node class="integer" name="columns">
      <node-attribute name="value" value="80" />
   </node>
   <node class="integer" name="webSocketTimeout">
      <node-attribute name="value" value="-1" />
   </node>
   <node class="integer" name="watchdogTimeout">
      <node-attribute name="value" value="300000" />
   </node>
   <node class="integer" name="port">
      <node-attribute name="value" value="9443" />
   </node>
   <node class="string" name="host">
      <node-attribute name="value" value="localhost" />
   </node>
</node>

Under The Hood

This section documents the implementation details of the web client.

Web Client Modes

The following shows the different modes of the web client.

TBD: More discussion, include ChUI as well.

Spawning a Web Client

TBD: Add descriptions for both embedded mode forms. Add spawner flow and description for ChUI.

This description is out of date. It needs to be updated to match the above diagram.

This section describes how the FWD Server and the remote FWD Client react, when the user accesses a Web Client:

  1. The process with the user opening a browser session with the FWD server (running an embedded Jetty instance on the same port as the administration console). The URL will look like https://localhost:<port>/chui , where <port> is the port configured in the adminPort directory configuration, in the server section (default or per-server). Depending on your browser configuration and how the server's SSL certificate was generated, the browser might show you an “untrusted connection” message and ask you to accept the certificate before continuing, on the first connection to this host and port.
  2. The FWD server sends down a login page, which looks like:
  3. The user inputs their operating system level user credentials (e.g. Linux userid and password) and submits the form.
  4. Using the spawner tool (see the Application Server Installation chapter for how to install the spawner), the FWD server thus launches a new FWD client (as a child process) and part of that "spawn" processing is an OS-level login. If the given credentials are authentic/correct, the child process is not only launched, but it has the user's context at the OS level. In other words, the process is run under the user's id and has full access to that user's files/configuration/rights.
  5. The FWD client has its own embedded web server. After successfully launching and initializing, it provides URL information back to the FWD server to allow a connection to the FWD client's web server.
  6. The FWD server sends a redirect response back to the user's initial browser session. This also includes an authorization token that allows the FWD client to ensure that only that browser session may access that web server.
  7. The user's browser session redirects to the FWD client's web server. This web server is ran using the server's certificate, so the browser might show you an “untrusted connection” message and ask you to accept the certificate before continuing, on the first connection to this host and port. And if the port is randomly assigned, this will mean each time a new port is reached, the “untrusted connection” message will be displayed. If you are encountering a HTTP ERROR: 403 - Forbidden page after the redirect, note at the time of this writing there is a limitation for web clients: the chuiWeb/host node needs to be configured so that it mentions the server's host used to access the web client.
  8. The FWD client's web server sends back the "main" page which is mostly Javascript.
  9. The loaded page establishes a bidirectional websocket to the FWD client.
  10. At this point the ChUI driver in the FWD client and the javascript code in the browser will communicate freely. The core idea here is that the page is not reloaded or refreshed, but instead the javascript code just handles the rendering of what looks like a terminal and it handles the forwarding of user input (keys in a ChUI environment).
  11. The FWD client processes as it normally would except that the ChUI driver delegates the final aspects of the UI to the browser side. Note that, depending on how the FWD application server is configured, the user might be required to enter distinct credentials, to access the application server (which may be different than the OS credentials, which are required to remotely launch the FWD Client).
  12. If the user exits the application normally or if it gets automatically disconnected (i.e. due to server shutdown), the browser will be redirected back to the initial login page.

Security

TBD: HTTPS only, how our certs work, spawner security, OS accounts for spawning (UNIX/Linux suid bit, Windows OS user auth), session redirect during auth/spawning, one-time auth token and web socket shift, all web resources are from server's jetty (initial auth) or client's jetty (actual web client usage)...


Remote Web Client Spawning via Brokers

This section describes how the FWD server, with the help of a broker client, spawns remote web (browser) GUI clients. Broker startup, registration, multi-broker deployments and broker selection are covered in the Remote Launchers chapter.

The configuration examples throughout are taken from the hotel_gui deployment — server deploy/server/directory.xml, broker client deploy/client/broker1_client.xml.

Overview

The FWD "broker" mechanism lets the FWD server delegate the launching of per-user web client JVMs to one or more broker hosts. A broker is itself an FWD client process started in a special broker mode that does not run any application UI; instead it logs into the server, registers as a remote launcher, and then waits for spawn requests. When a browser requests the web GUI, the server picks a registered broker, asks it (over an authenticated RPC connection) to start a new web client JVM, and the spawned web client then serves the browser its own embedded HTML5/Canvas UI.

Four distinct runtime actors are involved:

Actor Process / class Config file Role
FWD Server standard server (ServerDriver); hosts BrokerManager, WebClientsManager, WebDriverHandler / VirtualDesktopWebHandler deploy/server/directory.xml Directory/auth authority; serves the login landing page; selects a broker; allocates the client port; orchestrates the spawn.
FWD Client (Broker Client) BrokerCore in broker mode (<mode broker="true"/>) deploy/client/broker1_client.xml Authenticates to the server as the broker process account; registers; receives spawn RPCs; launches web client JVMs on its host via the native spawn binary.
FWD Spawned Web Client a fresh FWD client JVM (GuiWebDriver / ChuiWebDriver + embedded Jetty) launched by the broker runtime options injected by the broker/server Runs the actual 4GL session for one user; exposes an embedded web server that the browser connects to.
FWD Browser Web Client the end user's browser (p2j.js on HTML5 Canvas) Loads the landing page from the server, POSTs credentials, is redirected to the spawned web client's embedded server, then runs over a WebSocket.
Deployment / component view

Reference configuration

The two configuration files encode the identity, trust, and resource parameters that the whole mechanism depends on.

Server: directory.xml

The server side contributes four pieces of configuration that matter to the broker flow.

  • The broker process account* — accounts/processes/broker1_process:
<node class="process" name="broker1_process">
  <node-attribute name="enabled" value="TRUE"/>
  <node-attribute name="description" value="broker1_process"/>
  <node-attribute name="server" value="FALSE"/>
  <node-attribute name="alias" value="broker1"/>
</node>

This is the FWD process identity the broker client authenticates as. Its certificate alias is broker1.

  • The broker's certificate and private key* — under certificates:
  • certificates/peers/broker1 — the X.509 certificate (CN broker1 account) the server trusts as a peer.
  • certificates/private-keys/broker1 — the key entry + key password.

These correspond, on the client side, to the broker1-private-key.store keystore.

  • Authorization* — the broker process account must be able to read system/accounts, which BrokerManager.registerBroker() requires in order to confirm that the registering session belongs to a process account. The stock directory already grants this to all_others (entry 000500 of acl/system); a hardened directory that restricts accounts to named subjects must add the broker's process account explicitly:
<node class="container" name="000510">
  <node class="strings" name="subjects">
    <node-attribute name="values" value="broker1_process"/>
  </node>
  <node class="resource" name="resource-instance">
    <node-attribute name="reference" value="accounts"/>
  </node>
</node>

Without the grant, registration is rejected with "The broker client must be authenticated using a process account." even though the account is a valid process account, and web-client spawning silently falls back to local. See the Server Configuration section of the Remote Launchers chapter for the full set of ACLs a broker needs.

NOTE: Some deployments carry an acl/trustedspawner entry naming the broker. It is not enforced by the current engine and grants nothing; it may be removed. Authorization for broker spawning is instead the broker_account match described below, plus the spawn binary's own setuid privileges for the OS user.

  • The broker definition* — server/default/brokers/broker1:

<node class="container" name="brokers">
  <node class="broker" name="broker1">
    <node-attribute name="account" value="fwd"/>
    <node-attribute name="broker_account" value="broker1_process"/>
    <node-attribute name="host" value="192.168.100.28"/>
    <node class="container" name="portsRange">
      <node class="string"  name="namePrefix"><node-attribute name="value" value="client"/></node>
      <node class="integer" name="from"><node-attribute name="value" value="7449"/></node>
      <node class="integer" name="to"><node-attribute name="value" value="7459"/></node>
    </node>
  </node>
</node>

This is parsed by BrokerDiscovery.readBrokerConfigs() into Broker objects.

directory.xml attribute Broker field (BrokerDiscovery.getBroker()) Meaning
broker_account = broker1_process brokerAccount The process account the broker must authenticate as. Checked at registration.
account = fwd accounts (List) The FWD/OS user(s) this broker serves. Used to choose a broker for a given user.
host = 192.168.100.28 host The broker host; also the host the browser will ultimately reach.
portsRange/from = 7449 from First TCP port for spawned web clients on this host.
portsRange/to = 7459 to Last TCP port. portsRestricted = (to > from) && (from > 0).
Broker client: broker1_client.xml

The listing below is the working file from the reference deployment (hotel_gui/deploy/client/broker1_client.xml), with the passwords redacted:

<node type="client">
  <client>
    <mode broker="true" />                            <!-- BROKER MODE -->
  </client>

  <net>
    <server host="192.168.100.28" />
    <server port="3433" />                            <!-- inert while secure="true" -->
    <server secure_port="3333" />
    <connection secure="true" />
  </net>

  <security>
    <certificate validate="true" />
    <truststore filename="../server/srv-certs.store" />
    <truststore alias="standard" />
    <keystore filename="../server/broker1-private-key.store" />
    <keystore processalias="broker1" />               <!-- authenticate as broker1 -->
    <authentication type="program"/>                  <!-- no effect: processalias wins -->
  </security>

  <access>
    <password truststore="********" />                <!-- trust store file password -->
    <password keystore="********" />                  <!-- key store file password -->
    <password keyentry="********" />                  <!-- key entry password -->
<!--    <password masterkeyentry="********" />-->     <!-- server-side only; inert here -->
  </access>

  <broker>
    <retry count="10" />
    <retry seconds="10" />
  </broker>
  <client>
    <spawner value="/home/fwd/opt/hotel_gui/spawn" /> <!-- setuid native spawn binary -->
    <jvmArgs value="-Xmx1G -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true
                    -Djava.library.path=.../deploy/lib/ -Dfile.encoding=UTF-8
                    -Xdebug -Xnoagent -Djava.compiler=NONE
                    -Xrunjdwp:transport=dt_socket,address=9988,server=y,suspend=n" />
    <classpath value=".../deploy/lib/p2j.jar" />
    <minAgentPort value="9988" />
    <maxAgentPort value="9999" />
    <web host="192.168.100.28"/>                      <!-- dedicatedHost advertised -->
    <web dedicatedMode="false"/>                      <!-- network-accessible clients -->
    <web defaultOsUser="fwd"/>                        <!-- OS user spawned clients run as -->
  </client>
</node>

The jvmArgs value is a single attribute on one line in the real file; it is wrapped here for readability. The OS user is the deployment's own account (fwd throughout this document).

How the file is read

BootstrapConfig parses any <node type="client"> file as a strict three-level tree: first-level elements are categories (client, net, security, access, broker), second-level elements are groups, and the attributes of a group element are the keys. Every value is an attribute value, so <server host="…"/> is the key net/server/host. Repeated group elements merge into one group — which is why host, port and secure_port are written as three separate <server …/> lines — and repeated categories merge too, which is why the working file may carry two <client> blocks (one for mode, one for the spawn parameters) with no ill effect. Anything not organised in exactly three levels is ignored, as are XML comments.

The broker client is launched with this file as its bootstrap configuration — client.sh -c broker1_client.xml, i.e. java com.goldencode.p2j.main.ClientDriver broker1_client.xml [-p <password>]. ClientDriver.start() reads client/mode/broker and hands over to BrokerCore.start(); BrokerCore.initialize() reads the broker and client categories, and the net / security / access categories are consumed by the transport and security layers while connecting.

Parameters: the client category
Key Example Default Meaning and how to set it
client/mode/broker true false Selects broker mode: ClientDriver runs BrokerCore instead of a UI client. Must be true in this file; it is the only thing that distinguishes a broker client from an ordinary one.
client/spawner /home/fwd/opt/hotel_gui/spawn ./spawn Absolute path to the native spawn binary on the broker host. Its parent directory becomes the working directory of every spawned client (ProcessBuilder.directory), so place it in the deployment's own tree. The binary must be setuid root to switch to defaultOsUser.
client/spawnerLaunchTimeout not set 30 (seconds) How long BrokerCore.spawn() waits for the spawn binary to return before reporting a failure. Raise it only on hosts where JVM start-up is genuinely slow.
client/jvmArgs -Xmx1G … -Xrunjdwp:…address=9988,… the broker JVM's own input arguments JVM arguments for each spawned client, split on spaces. Leave the key out to inherit the broker JVM's arguments verbatim (ManagementFactory.getRuntimeMXBean().getInputArguments()). Two substrings are also parsed as feature flags: -Xrunjdwp: enables the debug agent (its address= is the fallback single port) and -Dcom.sun.management.jmxremote=true enables JMX.
client/classpath …/deploy/lib/p2j.jar the broker JVM's own classpath Classpath for spawned clients. As with jvmArgs, omitting it inherits the broker's. The path is resolved on the broker host, so the deployment's jars must exist there at that path.
client/minAgentPort 9988 0 First port of the debug/JMX agent range on the broker host.
client/maxAgentPort 9999 0 Last agent port. When the range is valid, each spawned client is assigned distinct debug/JMX ports from it and the literal address= in jvmArgs is rewritten per spawn (see Broker-spawned web clients below). Leave both at 0 to disable per-spawn agent ports.
client/web/host 192.168.100.28 the host's own name (InetAddress.getLocalHost().getHostName()) The broker's dedicatedHost: reported to the server at registerBroker() and used as the host the browser is ultimately sent to. It must name the same machine as the host attribute of the matching brokers/<name> node in directory.xml (matched on resolved addresses, not on the literal string — see Host Indexing and Ordering), and it must be a network-reachable name or address: BrokerCore.initialize() logs a SEVERE message if it is not a valid host name or IP.
client/web/dedicatedMode false false false — spawned clients advertise the real client/web/host, so a remote browser can reach them directly. true — the advertised host is forced to localhost on the spawned client, which only works when a reverse proxy fronts the clients (see Reverse-proxy deployments below).
client/web/defaultOsUser fwd "" The OS account spawned clients run as, passed to registerBroker() as associatedUser. It is also the key under which this broker's host is registered in allHosts / dedicatedHosts, so it is what getHostsForUser() consults when scheduling a spawn (see Selecting a Broker for a Spawn Request). The account must exist on the broker host and be permitted by the spawn binary.
Parameters: the net category
Key Example Default Meaning and how to set it
net/server/host 192.168.100.28 localhost The FWD server's host, as reachable from the broker host. Also substituted into the spawn command line, so spawned clients connect back to the same server.
net/connection/secure true false Selects TLS for the broker↔server connection. This choice decides which port key is read, so it must agree with the port keys below. Keep it true: process authentication is certificate-based.
net/server/secure_port 3333 -1 The server's TLS port. Read when secure="true" (LeafSessionManager.connectDirect).
net/server/port 3433 -1 The server's plaintext port, read only when secure="false" (falling back to net/server/insecure_port). With secure="true" this line is inert — it is kept in the reference file so the configuration can be switched over without looking the number up again.
net/server/timeout not set 600000 (ms) Protocol timeout for the session the broker opens to the server.
Parameters: the security and access categories

These are what make the broker a trusted process rather than an anonymous client; they are the client-side half of the identity chain in Server: @directory.xml@ (a)/(b) above.

Key Example Meaning and how to set it
security/truststore/filename ../server/srv-certs.store The trust store holding the server's certificate. Relative paths resolve against the broker process's working directory. Loading a truststore also forces security/certificate/validate to true, and the store's own trust managers are used instead of the JRE defaults.
access/password/truststore (cleartext) Password of that trust store.
security/truststore/alias standard The server's alias in the trust store. On a broker client this is documentation rather than a control: the client-side TLS handshake trusts every certificate in the store (a SunX509 trust manager over the whole store). It is read on the server side and injected into spawned clients, so keep it consistent with the server's alias.
security/keystore/filename ../server/broker1-private-key.store The broker's own key store, holding the private key and certificate it authenticates with. This is the counterpart of certificates/peers/broker1 + certificates/private-keys/broker1 in directory.xml.
access/password/keystore (cleartext) Password of the key store file.
access/password/keyentry (cleartext) Password of the key entry inside that store; used to initialise the KeyManagerFactory. Distinct from the store password above.
security/keystore/processalias broker1 The key-entry alias to authenticate with, and the switch that selects process authentication: with a processalias set, SecurityManager sends AUTH_REQ_PROCESS. The alias must exist as a key entry in the key store (otherwise "misconfigured keystore: no matching alias"), and its certificate must be the one the server trusts as the broker_account's peer — see _Server: directory.xml@_ above. Mutually exclusive with @security/keystore/useralias; setting both is a configuration error.
security/authentication/type program Selects programmatic (as opposed to interactive) authentication — but it is consulted only when no processalias is set. In this file processalias is present, so this line has no effect; it is harmless and conventional to leave it.
security/certificate/validate true Request validation of the server's certificate. Specifying either a truststore or a keystore file sets it to true regardless, so this line documents the intent rather than changing behaviour. Its one real effect is when no truststore is configured: validate="true" then falls back to the JRE's default trust store.
access/password/masterkeyentry commented out A single password used to unlock directory-stored key entries when they carry no key-password node, with a per-alias access:password:keyentry-<alias> fallback. It is read by SecurityCache while loading the directory — a server-side path — so it does nothing in a broker client file and is correctly left commented out here.
Parameters: the broker category
Key Example Default Meaning and how to set it
broker/retry/count 10 10 How many times the broker retries the connection to the server. The counter is reset after every successful registration, so this bounds a single reconnect storm, not the process lifetime.
broker/retry/seconds 10 10 Delay between attempts. With the example values a broker tolerates a server outage of roughly 100 s before giving up. Raise both on deployments where the server restarts slowly.
Setting up a broker client file
  1. Start from the deployment's client config and add <client><mode broker="true"/></client>. Everything else below is additive.
  2. Point it at the servernet/server/host plus net/server/secure_port, with net/connection/secure="true". Verify from the broker host itself, not from the server.
  3. Install the two stores next to the deployment (the reference file uses ../server/) and fill in all three access/password/* values: truststore, keystore, and key entry.
  4. Set security/keystore/processalias to the certificate alias of the process account that the matching brokers/<name> node names in broker_accountbroker1broker1_process in the reference configuration. This is the single most common misconfiguration: an alias that exists in the key store but is not the one the directory expects registers a session that is then rejected with "Account %s is not authorized to register broker %s".
  5. Set the spawn parametersclient/spawner (absolute path to the setuid spawn binary), client/classpath, and client/jvmArgs. Omit the last two only if the broker JVM is started with exactly the arguments and classpath the clients need.
  6. Give the host an agent port rangeclient/minAgentPort/maxAgentPort — if concurrent clients must be debuggable or JMX-visible. Otherwise leave both unset.
  7. Declare the web-facing identityclient/web/host (same machine as the broker's host in directory.xml), client/web/dedicatedMode, client/web/defaultOsUser (an existing OS account on that host).
  8. Tune the retry loop if the server is restarted often, then launch with client.sh -c broker1_client.xml and confirm registration in the server log: "Broker user=… uuid=… registered." A SEVERE "Connection Exception" on the broker side, or "No broker is defined for host …" on the server side, points back to steps 2, 4, or 7. The registration handshake these messages come from is described in Broker Startup and Registration.

NOTE: The browser-facing portsRange is not configured here. The broker client declares only the agent ports; the web-client ports for this host live on the brokers/<name> node in directory.xml (see Server: @directory.xml@ (d) above), because the server allocates them before the spawn request is sent. See Broker-spawned web clients for the distinction between the two ranges, and External proxy: client routing names and the proxy map for how the client ports become proxy routes.

WARNING: This file contains three cleartext passwords and the identity that lets a process register as a trusted broker. Restrict it to the broker's OS account (the reference deployment ships it 0660), keep it out of the application's version control, and consider storing it encrypted — java com.goldencode.p2j.cfg.BootstrapConfig encrypt <in> <out> produces an encrypted copy that ClientDriver accepts when launched with -p <password> (-p ? prompts interactively).

Identity & trust chain (how the two files line up)

Spawning a web client for a browser

End-to-end sequence
  1. Landing page — the browser does GET /gui (or /chui). VirtualDesktopWebHandler.handle()serveLandingPage() returns the login HTML.
  2. Login POST — the browser POSTs credentials. WebDriverHandler.handleStartClientRequest() extracts WebDriverRequestParameters (user, password, theme, device id, and any reverse-proxy X-Forwarded-Host/-Proto) and builds WebDriverSpawnParameters (driver type, SSO flag, options), then calls spawnWorker(...).
  3. Resource allocationWebClientsManager.allocateClient(proxyConfigs, host, uuid, osUser) polls one WebAllocatedResources (a free port, e.g. 7449, plus agent ports) from the broker host's pre-filled queue, tests that the port is actually free, and records the uuid → resources mapping.
  4. Broker selection & spawn RPCspawnWorker invokes WebClientSpawner.spawn(...), which (because this is a remote/broker spawn) routes through BrokerManager.spawn(clientBuilder, dedicatedUser, brokers, args, allocator, requestParameters, uuid):
    1. schedule(...) picks a registered broker for the user — honoring account=fwd / dedicated-host constraints and choosing the lowest getSystemLoading(). See Selecting a Broker for a Spawn Request for the full selection rules.
    2. updateWebClientOptions() injects the allocated --port=7449 (and reverse-proxy host/proto/web-root if present) into the command.
    3. The server calls broker.getRemote().spawn(BrokerSpawnParameters) over the broker's RPC session. BrokerSpawnParameters carries the command line + environment; trusted-vs-password mode decides whether a one-time password is streamed over stdin.
  5. Process launch on the brokerBrokerCore.spawn(args) (running on the broker host):
    1. Resolves command placeholders against the broker's local config — PARAM_SPAWNERspawner path, PARAM_JVMjvmArgs, PARAM_CPclasspath, PARAM_HOST/PARAM_PORT → server endpoint, PARAM_REMOTE_AGENT_HOSTdedicatedHost, PARAM_DEDICATED_MODEdedicatedMode.
    2. Runs the native spawn binary via ProcessBuilder (as OS user fwd, per the trusted-spawner grant), optionally writing the one-time password to stdin, and waits up to the launch timeout.
  6. Spawned web client comes up — the new JVM authenticates to the server with its temporary credentials and runs WebClientSpawner.TemporaryClientTask.doWork():
    1. Imports the server key store into its config, creates a GuiWebDriver (or ChuiWebDriver), and calls EmbeddedWebServer.startupServer(...) which starts an embedded Jetty server on host:port (e.g. 192.168.100.28:7449). If a reverse proxy is configured, the URI is rewritten using X-Forwarded-Host/-Proto and the proxy web-root.
    2. It calls back to the server via RPC spawner.clientIsReady(uuid, uri). On the server, WebClientSpawner.clientIsReady() stores this as remoteUri.
  7. Session registration — when the spawned client's P2J session attaches, WebClientsManager.registerWebClientSession(webClientId, peerNode) cancels the cleanup timer and maps peerNode → WebAllocatedResources, so the port can be released when the session ends (freeClient() returns it to the queue).
  8. Redirect to the browser — back in spawnWorker, after exitCode == 0, result.uri = spawner.getRemoteUri() appends the authorization token (index.html?token=...). The server returns this URL to the browser (as a JSON/body redirect).
  9. Browser connects to the spawned client — the browser navigates to the spawned client's embedded server URI (192.168.100.28:7449, directly or via the reverse proxy) and opens the WebSocket; from then on the canvas UI is driven by the spawned web client. Because dedicatedMode=false, the advertised host is the real 192.168.100.28 rather than localhost, so a remote browser can reach it.
Spawn sequence diagram

Reverse-proxy deployments

The browser ultimately connects to a spawned web client's embedded server (e.g. 192.168.100.28:7449). In production that host:port is usually fronted by a reverse proxy (nginx, Apache, a load balancer) so that browsers see a single public hostname over standard TLS, and the internal 7449..7459 ports are never exposed directly.

How the proxy headers flow through the spawn
  1. The browser hits the proxy, which forwards the login request to the FWD server adding X-Forwarded-Host and X-Forwarded-Proto headers.
  2. WebDriverHandler extracts these into WebDriverRequestParameters (forwardedHost, forwardedProto, plus externalUserIp) and passes them to WebClientSpawner.spawn(webClientsManager, {forwardedHost, forwardedProto, externalUserIp}).
  3. BrokerManager.updateWebClientOptions() injects the proxy values (PROXY_HOST, PROXY_PROTOCOL, WEB_ROOT) into the spawn command alongside the allocated port.
  4. On the spawned client, TemporaryClientTask.doWork() starts the embedded server on the internal host:port and then, if forwardedHost is set, rewrites the externally visible URI with getForwardedUri(forwardedProto, forwardedHost, webRoot) — i.e. URI.create(forwardedProto + "://" + forwardedHost + webRoot + "/").
  5. The rewritten URI is reported back to the server via clientIsReady(uuid, uri); WebDriverHandler.getRemoteUri() appends the auth token, and the browser is redirected to the public proxy URL — never to the internal port.
Proxy path (web-root) configuration

WebDriverHandler.getServerProxyPath(forwardedHost) returns "/" + PROXY_PATH when a proxy custom context path (serverProxyPath) is configured in the directory webClient config (and an empty string otherwise). This proxyPath / web-root is used both to build login/logout page URLs (PLACEHOLDER_PROXY_PATH, web:url:serverBaseUrl) and to rewrite the spawned client's URI, so that a proxy mapping like https://public.example.com/fwd/ → internal 192.168.100.28:7449/ resolves consistently. (When REMOVE_PROXY_PATH_IN_WEB_PAGES is set, the proxy path is stripped from page URLs so the proxy can map serverProxyPath directly to the driver root; in that case the directory serverProxyPath and guiIdentifier/chuiIdentifier should match.)

External proxy: client routing names and the proxy map

When the proxy is an external component (nginx, Apache, a load balancer) rather than something FWD controls, the two sides never exchange the allocated port at run time. Instead they agree on a stable name per client port. At allocation time WebClientsManager.allocateClient() builds the spawned client's web-root as "/" + proxyPathSegment + "/" + ClientsToPortsGenerator.getPortName(...) — but only when a forwarded host is in effect; without a proxy the web-root stays null and the browser is sent to the internal host:port directly. The name is:

<namePrefix>-<hostIndex>-<offset>        offset = port - from + 1

so a spawned client on the first declared broker's first port is advertised to the browser as https://public.example.com/proxy/client-2-1/ (WebClientSpawner.getForwardedUri()).

The three inputs come from different places, and their scopes differ:

Input Directory node Default Scope
proxyPathSegment webClient/proxyPathSegment proxy deployment-wide
namePrefix webClient/portsRange/namePrefix client deployment-wide — cannot be set per broker
hostIndex broker declaration order + 1; 1 is the server's loopback host
offset portsRange/from relative to that host's own from

hostIndex is not configured anywhere: it is the broker's 1-based position in the brokers container of directory.xml, plus one, with index 1 permanently reserved for the FWD server's own loopback host. Reordering, adding or removing <broker> nodes therefore renames every client route at and after that position, and a broker declared on the loopback breaks the mapping outright. See Host Indexing and Ordering for the full rule and the startup validation that enforces it.

namePrefix is a Type.WEB_CLIENT config item, so it resolves only at webClient/portsRange/namePrefix or clientConfig/portsRange/namePrefix; unlike portsRange/from/to — which BrokerDiscovery reads per broker node via Type.RELATIVE_TO_PARENT — it has no per-broker form. That is harmless, because uniqueness is carried entirely by hostIndex: every host has a distinct index, so one shared prefix cannot collide. The prefix is a namespace label for the whole deployment — choose it so FWD's locations do not clash with anything else the proxy serves, and give two FWD deployments behind one proxy different namePrefix (or proxyPathSegment) values.

Because offset is relative to each host's own from, brokers with different port ranges both start at …-1, and the name therefore encodes no absolute port:

Broker (declaration order) host portsRange hostIndex Generated names → backend
broker1 (1) 192.168.100.28 7449–7451 2 client-2-1:7449, client-2-2:7450, client-2-3:7451
broker2 (2) 192.168.100.29 8000–8002 3 client-3-1:8000, client-3-2:8001, client-3-3:8002

Identical ranges on different hosts are equally valid, and are the usual case — the hosts are told apart by index, not by port.

The name → host:port binding therefore exists in exactly one place: the map file produced offline by

java com.goldencode.p2j.main.ClientsToPortsGenerator <directory.xml> [host]

which writes map.clients-to-backends — one <name> <host>:<port> line per port, per broker in declaration order (local mode, with no brokers configured, emits a single host at index 1, using the optional host argument or localhost). Those lines are what the proxy configuration is built from. For nginx:

map $request_uri $target_upstream {
    ~^/proxy/client-2-      192.168.100.28;
    ~^/proxy/client-3-      192.168.100.29;
}

map $request_uri $new_upstream_port {
    ~^/proxy/client-2-1/(.*)$   7449;
    ~^/proxy/client-2-2/(.*)$   7450;
    ~^/proxy/client-2-3/(.*)$   7451;
    ~^/proxy/client-3-1/(.*)$   8000;
    ~^/proxy/client-3-2/(.*)$   8001;
    ~^/proxy/client-3-3/(.*)$   8002;
}

Configuring the proxy settings

Every proxy knob except the per-broker port ranges lives in the webClient container of directory.xml (server/default/webClient in the reference configuration); the per-broker ranges live on each brokers/<name> node (see Server: @directory.xml@ (d) above).

Node under server/default/webClient Class Default Purpose
portsRange/from, portsRange/to integer none Browser-facing embedded-web-server ports for the local (loopback, index 1) host only. Broker hosts take their ranges from their own brokers/<name>/portsRange.
portsRange/namePrefix string client First component of the routing name. Deployment-wide.
portsRange/forwardedHost string none — falls back to the X-Forwarded-Host header Pins the public host[:port] the browser is redirected to.
portsRange/forwardedProto string https Pins the public scheme.
proxyPathSegment string proxy First path segment of every spawned-client web-root.
serverProxyPath string none Extra context path the proxy prepends to the FWD server's own pages (login, logout, virtual desktop). Applied only when a forwarded host is in effect.
removeProxyPathInWebPages boolean FALSE Strip serverProxyPath from generated page URLs, so the proxy may map its public prefix straight onto the driver root.

Setting them up:

  1. Decide the public entry point. The proxy terminates TLS on a single public name (https://public.example.com) and must forward X-Forwarded-Host and X-Forwarded-Proto to the FWD server. If it cannot be made to send those headers, pin portsRange/forwardedHost and portsRange/forwardedProto instead — but read the last note in this section first, because a pinned value applies to every session.
  2. Give every client host a port range. The loopback host uses webClient/portsRange; each broker host uses its own brokers/<name>/portsRange (mandatory — see the IMPORTANT note below). These ports need to be reachable from the proxy, not from browsers.
  3. Set the naming knobsproxyPathSegment and namePrefix — if the defaults (proxy, client) are not wanted. Both are deployment-wide; see the webClient excerpt below.
  4. Add serverProxyPath only if the FWD server is not mounted at the proxy root. With serverProxyPath = fwd the login page is advertised as https://public.example.com/fwd/gui, so the proxy must map /fwd/… onto the server. If instead the proxy maps its public prefix directly onto the driver root (/fwd/gui), also set removeProxyPathInWebPages to TRUE and make serverProxyPath equal to guiIdentifier / chuiIdentifier (defaults gui / chui), otherwise the page URLs carry the prefix twice.
  5. Generate the map — run ClientsToPortsGenerator (above) after the directory is final, and keep map.clients-to-backends next to the proxy configuration it feeds.
  6. Write the proxy rules — there are two distinct classes of route. (a) Server routes: the driver roots /gui and /chui (renamed by guiIdentifier / chuiIdentifier), the shared resources under /webres, and — if set — all of that under /<serverProxyPath>, all pointing at the FWD server's own host:port. (b) Client routes: /<proxyPathSegment>/<name>/ → the host:port from the map, one per line of the map file, as in the nginx snippet above. Both classes must permit the WebSocket upgrade, since the canvas session runs over WebSocket on the client route.
  7. Verify end to end. Log in through the public name: the redirect must land on https://public.example.com/proxy/client-2-1/… (never on an internal host:port), and the WebSocket must upgrade on that same path. A redirect to an internal address means no forwarded host reached the server; a 404 on a correct-looking path means the map and the running configuration disagree.

The corresponding webClient excerpt — steps 2 to 4, with the loopback host's own range included:

<node class="container" name="webClient">
  ...
  <node class="string" name="proxyPathSegment"><node-attribute name="value" value="proxy"/></node>
  <node class="string" name="serverProxyPath"><node-attribute name="value" value="fwd"/></node>
  <node class="container" name="portsRange">
    <node class="string"  name="namePrefix"><node-attribute name="value" value="client"/></node>
    <node class="integer" name="from"><node-attribute name="value" value="7449"/></node>
    <node class="integer" name="to"><node-attribute name="value" value="7459"/></node>
  </node>
</node>

NOTE: namePrefix must be set under webClient/portsRange. The reference broker definition under _Server: directory.xml@_ (d) above also shows a @namePrefix node inside brokers/broker1/portsRange — that node is never read: PROXY_CLIENT_NAME_PREFIX is a Type.WEB_CLIENT config item and resolves only at webClient/portsRange/namePrefix or clientConfig/portsRange/namePrefix, so a prefix placed on a broker node is silently ignored and the default client is used.

IMPORTANT: Behind an external proxy, every <broker> node must declare its own portsRange/from and portsRange/to. The global webClient/portsRange is not inherited by brokers — it configures only the FWD server's own loopback host (index 1). A broker without a range is not ports-restricted, and the spawn then fails in WebClientsManager.allocateClient() with "Correct range: 'webClient/portsRange/from' < 'webClient/portsRange/to' must be provided" — a message that names the global node even though the missing one is brokers/<name>/portsRange.

WARNING: Nothing validates the map against the running server: the server derives the web-root from live broker registrations, while the proxy resolves it from a file generated earlier. When they disagree the client is simply unreachable — a proxy 404/502 with no FWD-side error. Regenerate map.clients-to-backends and reload the proxy after any of: adding, removing or reordering <broker> nodes (every index at and after that position shifts); changing any broker's from (same names, different ports — the worst case, since it can route to a live but wrong port) or to (different name count); changing namePrefix; changing proxyPathSegment.

NOTE: The name separators are hyphens (client-2-1) as of the 20260513 change to ClientsToPortsGenerator.getPortName() (refs: #11454-22, #11454-26), which removed the ambiguity of the older un-hyphenated form (client211 — host 2 port 11, or host 21 port 1?). Proxy configurations written against the older form, including the nginx.conf example in the FWD tree, do not match the current web-roots and must be regenerated.

NOTE: When the proxy does not add X-Forwarded-Host/X-Forwarded-Proto, the forwarded values can be pinned in the directory as webClient/portsRange/forwardedHost and webClient/portsRange/forwardedProto (PROXY_HOST / PROXY_PROTOCOL). A configured forwardedHost takes precedence over the headersWebDriverHandler.resolveForwardedHost() returns the directory value whenever it is set and only falls back to the request header otherwise, and the protocol follows the same rule (defaulting to https). Setting it therefore forces every session through the proxy view, so leave it unset in deployments that must also serve direct, non-proxied browsers.

Reverse-proxy flow


Debugging spawned web clients

There are locally-spawned and broker-spawned web clients.

Locally-spawned web clients

These special directory settings under clientConfig node start the java debugger agent on the specified ports and apply the default debugger profile settings to spawned java clients.

Configuration name Class Format/Example
minAgentPort integer <node class="integer" name="minAgentPort"><node-attribute name="value" value="8980"/></node>
maxAgentPort integer <node class="integer" name="maxAgentPort"><node-attribute name="value" value="8990"/></node>
enableSpawnerDebug boolean <node class="boolean" name="enableSpawnerDebug"><node-attribute name="value" value="TRUE"/></node>
jvmArgs string <node class="string" name="jvmArgs"><node-attribute name="value" value="*"/></node>

jvmArgs value="*" is a standard JVM arguments string. For the debugging the following JVM arguments string is used commonly in FWD project

-Xmx512m
-Djava.awt.headless=true
-Duser.country=US
-Duser.language=en
-Xdebug
-Xnoagent
-Djava.compiler=NONE
-Djava.net.preferIPv4Stack=true
-Xrunjdwp:transport=dt_socket,address=8980,server=y,suspend=n

All these options can be adjusted for the target tasks.

If jvmArgs settings specifies the java debugger -Xrunjdwp:transport=dt_socket,address=8980,server=y,suspend=n, then for the locally-spawned java process the given port number is allocated 8980. So the next spawned process cannot use the same settings and these configuration nodes minAgentPort and maxAgentPort are used to specify the range of available ports for the java debugger. The web client runtime manages these ports so the next spawned java process can be started successfully in this debug mode.

This is an example of the directory settings used here

        <node class="container" name="clientConfig">
          ............................................
          <node class="boolean" name="enableSpawnerDebug">
            <node-attribute name="value" value="TRUE"/>
          </node>
          <node class="integer" name="minAgentPort">
            <node-attribute name="value" value="8980"/>
          </node>
          <node class="integer" name="maxAgentPort">
            <node-attribute name="value" value="8999"/>
          </node>
          <node class="string" name="jvmArgs">
            <node-attribute name="value" value="-Xmx512m
                                                -Djava.awt.headless=true
                                                -Duser.country=US
                                                -Duser.language=en
                                                -Xdebug
                                                -Xnoagent
                                                -Djava.compiler=NONE
                                                -Djava.net.preferIPv4Stack=true
                                                -Xrunjdwp:transport=dt_socket,address=8980,server=y,suspend=n"/>
          </node>
          <node class="string" name="cfgOverrides">
            <node-attribute name="value" value="client:cmd-line-option:debugalert=true net:http_client:disable_ssl_certificate_validation=true security:provider:name=conscrypt"/>
          </node>
          ...................................................
        </node>
Broker-spawned web clients

When a client is launched on a remote broker host rather than locally, the equivalent debug/agent port management is driven from the broker client's own client/minAgentPort and client/maxAgentPort (broker1_client.xml, see Remote Web Client Spawning via Brokers above) rather than from the clientConfig directory nodes.

Do not confuse the two port ranges involved. The agent ports (9988–9999 in the reference broker client file) are the JVM debug/JMX ports for the spawned process on the broker host. The client ports (portsRange, 7449–7459, from the brokers/<name> node in directory.xml) are the browser-facing embedded-web-server ports. They are allocated and tracked separately.

As with locally-spawned clients, the fixed JDWP port written into jvmArgs (-Xrunjdwp:...address=9988) is not used as-is for concurrent clients: when an agent port range is configured it overrides that literal per spawn. Each spawned client on the broker host is assigned a distinct debug and JMX port from the range, and the address= value in the spawned JVM's command line is rewritten to the allocated port, so concurrent clients do not collide.

This section will help the end-user with some suggestions regarding the recommended values for the parameters which can be defined in directory.xml. It will include a set of values for environments with fast and reliable network, and another set for the ones with slow networks. The description for each of the following parameters can be read in the bootstrap configuration, corresponding to the client:web entries.

Configuration name Default Fast network Slow network
webSocketTimeout 90000 90000 180000
watchdogTimeout 120000 90000 180000
pingPongInterval 30000 15000 50000
maxLostPings 6 4 10
delayBetweenPingTries 1000 1000 3000
delayBetweenTriesToConnect 5000 2500 8000
maxIdleTime 90000 90000 180000
embWebServerMaxIdleTimeout 90000 90000 180000
maxOutputBufferSize 1048576 1048576 524288
maxOutputAggregationSize 8192 8192 4096
maxResponseHeaderSize 8192 8192 4096
maxRequestHeaderSize 8192 8192 4096

There are restrictions on values used for configuration parameters: the embedded web server parameters for the java web client webSocketTimeout, watchdogTimeout, embWebServerMaxIdleTimeout and maxIdleTime and the jscript client parameter pingPongInterval.

webSocketTimeout <= maxIdleTime <= embWebServerMaxIdleTimeout, webSocketTimeout + pingPongInterval <= watchdogTimeout and webSocketTimeout > pingPongInterval should be satisfied so that for watchdogTimeout the web client can reconnect to the java web client and at least one ping message comes within webSocketTimeout interval.

  • maxIdleTime used to set org.eclipse.jetty.websocket.servlet.WebSocketServletFactory.getPolicy().setIdleTimeout(maxIdleTime) for the web socket connection created by web socket servlet factory. If its directory value is not set, then the default value for the web client is 90000 ms given by maxIdleTime=90000.
  • webSocketTimeout used to set a timeout org.eclipse.jetty.websocket.api.Session.setIdleTimeout(webSocketTimeout) for the given web socket session. If its directory value is not set, then the default value is 90000 ms given by webSocketTimeout=90000.
  • embWebServerMaxIdleTimeout used to set a timeout for idle http connection of the embedded web server by org.eclipse.jetty.server.HttpConfiguration.setIdleTimeout(embWebServerMaxIdleTimeout). If its directory value is not set, then the default value is infinity given by embWebServerMaxIdleTimeout=0.
  • watchdogTimeout is a timeout within which interval the java web client can establish the web socket connection with the jscript web client on any IO exceptions during network operations. If its directory value is not set, then the default value is 120000 ms given by watchdogTimeout=120000.
  • pingPongInterval used for jscript web client that sends a ping pong message within this interval. If its directory value is not set, then the default value is 30000 ms given by pingPongInterval=30000.
  • clientResponseTimeout is the time in milliseconds the java web client waits for a response from the jscript web client before giving up, so the java side is never blocked indefinitely. It applies when the java client announces that it is shutting down and waits for the jscript client to acknowledge, which gives the browser a chance to clear its authorization cookie first. If its directory value is not set, or is set to a non-positive value, the default value is 5000 ms.
  • authCookieTtl is the lifetime in seconds of the web client authorization cookie. While a browser tab is active the cookie is periodically refreshed; once the tab is closed the cookie expires within this interval, which reclaims cookies orphaned by abnormally-closed tabs. If its directory value is not set, or is set to a non-positive value, the default value is 1800 seconds (30 minutes).

The target default values are defined in com.goldencode.p2j.web.WebConfigurationConstants.

© 2004-2026 Golden Code Development Corporation. ALL RIGHTS RESERVED.

client_spawning_flow.png (191 KB) Greg Shah, 03/29/2019 04:24 PM

virtual_desktop_mode_os_account_authentication_prompt.png (6.07 KB) Greg Shah, 03/09/2020 05:02 AM

web_client_modes.png (182 KB) Greg Shah, 03/09/2020 05:04 AM

web_client_spawning_flow_external_embedded_mode.png (241 KB) Greg Shah, 03/09/2020 05:04 AM

web_client_spawning_flow_integrated_embedded_mode.png (231 KB) Greg Shah, 03/09/2020 05:04 AM

web_client_spawning_flow_virtual_desktop_mode.png (172 KB) Greg Shah, 03/09/2020 05:04 AM

broker_reverse_proxy.png (95.8 KB) Sergey Ivanovskiy, 06/25/2026 07:04 AM

broker_scheduling.png (121 KB) Sergey Ivanovskiy, 06/25/2026 07:04 AM

broker_spawn_sequence.png (90.5 KB) Sergey Ivanovskiy, 06/25/2026 07:04 AM

broker_registration.png (146 KB) Sergey Ivanovskiy, 06/25/2026 07:04 AM

broker_identity_trust.png (127 KB) Sergey Ivanovskiy, 06/25/2026 07:04 AM

broker_deployment.png (85.8 KB) Sergey Ivanovskiy, 06/25/2026 07:04 AM

broker_reverse_proxy.mmd (865 Bytes) Sergey Ivanovskiy, 06/25/2026 07:10 AM

broker_scheduling.mmd (547 Bytes) Sergey Ivanovskiy, 06/25/2026 07:10 AM

broker_spawn_sequence.mmd (1.48 KB) Sergey Ivanovskiy, 06/25/2026 07:10 AM

broker_registration.mmd (1011 Bytes) Sergey Ivanovskiy, 06/25/2026 07:10 AM

broker_identity_trust.mmd (813 Bytes) Sergey Ivanovskiy, 06/25/2026 07:10 AM

broker_deployment.mmd (1.15 KB) Sergey Ivanovskiy, 06/25/2026 07:10 AM

broker_identity_trust.png (135 KB) Sergey Ivanovskiy, 07/29/2026 08:53 AM