Added --http-upgrade and --direct to ws-test dial, so it can connect to job-search serve or ws-test serve
One of the two is required. --http-upgrade upgrades an HTTP connection to a WebSocket on /ws, as before, for job-search serve; --url and --origin go with it. --direct connects straight to ws-test serve over plain TCP, one JSON message per line, at --addr (default from server.host and server.port). --ping, --message, --listen and stdin work the same in both modes. The README's ws-test section explains the two modes.
This commit is contained in:
1 parent
88f879d844
commit
f0c171c8a8
5 files changed
+506
-60
No files matched your search
+142
-49
@@ -30,9 +30,13 @@ import (
|
||||
// carries back.
|
||||
const pingID = "ping"
|
||||
|
||||
// dialSettings are the dial command's flag values.
|
||||
// dialSettings are the dial command's flag values. direct chooses a plain
|
||||
// TCP connection to addr (--direct) over an HTTP upgrade at url
|
||||
// (--http-upgrade).
|
||||
type dialSettings struct {
|
||||
direct bool
|
||||
url string
|
||||
addr string
|
||||
origin string
|
||||
messages []string
|
||||
ping bool
|
||||
@@ -40,14 +44,33 @@ type dialSettings struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// commandDial returns the "dial" command, which opens a WebSocket on the
|
||||
// HTTP server's /ws path, sends messages and prints the replies.
|
||||
// connection is what dial talks to: a *websocket.Conn with --http-upgrade,
|
||||
// or a *directConn with --direct.
|
||||
type connection interface {
|
||||
Read(ctx context.Context) (websocket.MessageType, []byte, error)
|
||||
Write(
|
||||
ctx context.Context,
|
||||
messageType websocket.MessageType,
|
||||
payload []byte,
|
||||
) error
|
||||
Close(code websocket.StatusCode, reason string) error
|
||||
CloseNow() error
|
||||
}
|
||||
|
||||
// commandDial returns the "dial" command, which connects to the WebSocket
|
||||
// hub of job-search serve (--http-upgrade) or ws-test serve (--direct),
|
||||
// sends messages and prints the replies.
|
||||
func commandDial(opts *cli.Options) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "dial",
|
||||
Short: "Open a WebSocket on /ws, send messages and print the replies",
|
||||
Long: `Upgrade an HTTP connection to a WebSocket on the server's /ws path and print
|
||||
every message the server sends, starting with its welcome.
|
||||
Short: "Connect to a WebSocket hub, send messages and print the replies",
|
||||
Long: `Connect to a server's WebSocket hub and print every message the server sends,
|
||||
starting with its welcome. One of these is required:
|
||||
|
||||
--http-upgrade upgrade an HTTP connection to a WebSocket on the /ws path,
|
||||
as job-search serve expects
|
||||
--direct connect straight to ws-test serve over plain TCP, with one
|
||||
JSON message per line in each direction
|
||||
|
||||
With --ping, it sends a ping and waits for the pong, failing if none arrives
|
||||
within --timeout.
|
||||
@@ -62,10 +85,13 @@ With neither, each line read from stdin is sent as a message (blank lines are
|
||||
skipped), until Ctrl+C is pressed or the server closes the connection. The end
|
||||
of stdin stops the sending, but the connection stays open.
|
||||
|
||||
The URL defaults to the configured server.host and server.port (wss:// if
|
||||
server.tls.enabled is set). Set --origin to test the Origin check: an upgrade
|
||||
from an origin that isn't the server's own or one of
|
||||
server.csrf.cross_origin_protection.trusted_origins is refused with 403.`,
|
||||
With --http-upgrade, the URL defaults to the configured server.host and
|
||||
server.port (wss:// if server.tls.enabled is set). Set --origin to test the
|
||||
Origin check: an upgrade from an origin that isn't the server's own or one of
|
||||
server.csrf.cross_origin_protection.trusted_origins is refused with 403.
|
||||
|
||||
With --direct, --addr defaults to the configured server.host and server.port.
|
||||
There is no HTTP request, so --url and --origin don't apply.`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("command persistent pre-run", "command", "dial")
|
||||
},
|
||||
@@ -81,12 +107,17 @@ server.csrf.cross_origin_protection.trusted_origins is refused with 403.`,
|
||||
return err
|
||||
}
|
||||
|
||||
if settings.url == "" {
|
||||
// Only one of url and addr can be set, for the chosen mode.
|
||||
if settings.url == "" && settings.addr == "" {
|
||||
conf, err := opts.RunSetup(cmd, true, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load configuration: %w", err)
|
||||
}
|
||||
settings.url = defaultURL(conf.Server)
|
||||
if settings.direct {
|
||||
settings.addr = defaultAddr(conf.Server)
|
||||
} else {
|
||||
settings.url = defaultURL(conf.Server)
|
||||
}
|
||||
}
|
||||
|
||||
// ctx is cancelled on the first SIGINT (Ctrl+C) or SIGTERM.
|
||||
@@ -100,15 +131,34 @@ server.csrf.cross_origin_protection.trusted_origins is refused with 403.`,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.Bool(
|
||||
"http-upgrade",
|
||||
false,
|
||||
"upgrade an HTTP connection to a WebSocket on /ws (job-search serve)",
|
||||
)
|
||||
flags.Bool(
|
||||
"direct",
|
||||
false,
|
||||
"connect straight over plain TCP, one JSON message per line "+
|
||||
"(ws-test serve)",
|
||||
)
|
||||
flags.String(
|
||||
"url",
|
||||
"",
|
||||
"WebSocket URL (default from config, ws://127.0.0.1:9393/ws)",
|
||||
"with --http-upgrade, the WebSocket URL "+
|
||||
"(default from config, ws://127.0.0.1:9393/ws)",
|
||||
)
|
||||
flags.String(
|
||||
"addr",
|
||||
"",
|
||||
"with --direct, the address to connect to "+
|
||||
"(default from config, 127.0.0.1:9393)",
|
||||
)
|
||||
flags.String(
|
||||
"origin",
|
||||
"",
|
||||
"Origin header to send, as a browser would (default none)",
|
||||
"with --http-upgrade, the Origin header to send, as a browser would "+
|
||||
"(default none)",
|
||||
)
|
||||
flags.StringArray(
|
||||
"message",
|
||||
@@ -129,8 +179,13 @@ server.csrf.cross_origin_protection.trusted_origins is refused with 403.`,
|
||||
flags.Duration(
|
||||
"timeout",
|
||||
10*time.Second,
|
||||
"how long to wait for the upgrade and for each reply; 0s for no limit",
|
||||
"how long to wait to connect and for each reply; 0s for no limit",
|
||||
)
|
||||
cmd.MarkFlagsOneRequired("http-upgrade", "direct")
|
||||
cmd.MarkFlagsMutuallyExclusive("http-upgrade", "direct")
|
||||
cmd.MarkFlagsMutuallyExclusive("direct", "url")
|
||||
cmd.MarkFlagsMutuallyExclusive("direct", "origin")
|
||||
cmd.MarkFlagsMutuallyExclusive("http-upgrade", "addr")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -139,7 +194,9 @@ server.csrf.cross_origin_protection.trusted_origins is refused with 403.`,
|
||||
// commandDial, so getting them can't fail.
|
||||
func dialFlags(cmd *cobra.Command) (dialSettings, error) {
|
||||
flags := cmd.Flags()
|
||||
direct, directErr := flags.GetBool("direct")
|
||||
url, urlErr := flags.GetString("url")
|
||||
addr, addrErr := flags.GetString("addr")
|
||||
origin, originErr := flags.GetString("origin")
|
||||
messages, messagesErr := flags.GetStringArray("message")
|
||||
ping, pingErr := flags.GetBool("ping")
|
||||
@@ -147,7 +204,9 @@ func dialFlags(cmd *cobra.Command) (dialSettings, error) {
|
||||
timeout, timeoutErr := flags.GetDuration("timeout")
|
||||
|
||||
err := errors.Join(
|
||||
directErr,
|
||||
urlErr,
|
||||
addrErr,
|
||||
originErr,
|
||||
messagesErr,
|
||||
pingErr,
|
||||
@@ -159,7 +218,9 @@ func dialFlags(cmd *cobra.Command) (dialSettings, error) {
|
||||
}
|
||||
|
||||
return dialSettings{
|
||||
direct: direct,
|
||||
url: url,
|
||||
addr: addr,
|
||||
origin: origin,
|
||||
messages: messages,
|
||||
ping: ping,
|
||||
@@ -169,59 +230,46 @@ func dialFlags(cmd *cobra.Command) (dialSettings, error) {
|
||||
}
|
||||
|
||||
// defaultURL is the /ws URL of the server conf configures: ws://, or wss://
|
||||
// with TLS, on its host and port. An empty host (every interface) is dialled
|
||||
// as 127.0.0.1.
|
||||
// with TLS, at defaultAddr.
|
||||
func defaultURL(conf config.JSCServer) string {
|
||||
scheme := "ws"
|
||||
if conf.TLS.Enabled {
|
||||
scheme = "wss"
|
||||
}
|
||||
|
||||
return scheme + "://" + defaultAddr(conf) + "/ws"
|
||||
}
|
||||
|
||||
// defaultAddr is the host:port of the server conf configures. An empty host
|
||||
// (every interface) is dialled as 127.0.0.1.
|
||||
func defaultAddr(conf config.JSCServer) string {
|
||||
host := conf.Host
|
||||
if host == "" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
|
||||
address := net.JoinHostPort(host, strconv.Itoa(conf.Port))
|
||||
|
||||
return scheme + "://" + address + "/ws"
|
||||
return net.JoinHostPort(host, strconv.Itoa(conf.Port))
|
||||
}
|
||||
|
||||
// dial opens a WebSocket at settings.url and prints the messages received
|
||||
// to out, each after "<". It sends each of settings.messages, printed after
|
||||
// ">", or without them each line of input (see interact), which isn't
|
||||
// printed again.
|
||||
// dial connects as settings choose (see connect) and prints the messages
|
||||
// received to out, each after "<". It sends each of settings.messages,
|
||||
// printed after ">", or without them each line of input (see interact),
|
||||
// which isn't printed again.
|
||||
func dial(
|
||||
ctx context.Context,
|
||||
input io.Reader,
|
||||
out io.Writer,
|
||||
settings dialSettings,
|
||||
) error {
|
||||
header := http.Header{}
|
||||
if settings.origin != "" {
|
||||
header.Set("Origin", settings.origin)
|
||||
}
|
||||
|
||||
dialCtx, cancel := util.WithTimeout(ctx, settings.timeout)
|
||||
connectCtx, cancel := util.WithTimeout(ctx, settings.timeout)
|
||||
defer cancel()
|
||||
|
||||
conn, res, err := websocket.Dial(
|
||||
dialCtx,
|
||||
settings.url,
|
||||
&websocket.DialOptions{
|
||||
HTTPHeader: header,
|
||||
},
|
||||
)
|
||||
conn, err := connect(connectCtx, out, settings)
|
||||
if err != nil {
|
||||
if res != nil {
|
||||
return fmt.Errorf("upgrade refused: %s", res.Status)
|
||||
}
|
||||
return fmt.Errorf("dialling %s: %w", settings.url, err)
|
||||
return err
|
||||
}
|
||||
defer conn.CloseNow() //nolint:errcheck // finished with; it may already be closed
|
||||
|
||||
fmt.Fprintf(out, "connected (%s)\n", res.Status) //nolint:errcheck // printing only
|
||||
|
||||
if !settings.ping && len(settings.messages) == 0 {
|
||||
return interact(ctx, input, out, conn, settings.timeout)
|
||||
}
|
||||
@@ -260,12 +308,53 @@ func dial(
|
||||
return conn.Close(websocket.StatusNormalClosure, "")
|
||||
}
|
||||
|
||||
// connect opens a WebSocket at settings.url, or with settings.direct a
|
||||
// plain TCP connection to settings.addr, giving up when ctx is done. It
|
||||
// prints how it connected to out.
|
||||
func connect(
|
||||
ctx context.Context,
|
||||
out io.Writer,
|
||||
settings dialSettings,
|
||||
) (connection, error) {
|
||||
if settings.direct {
|
||||
conn, err := dialDirect(ctx, settings.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connecting to %s: %w", settings.addr, err)
|
||||
}
|
||||
fmt.Fprintf(out, "connected directly to %s\n", settings.addr) //nolint:errcheck // printing only
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
header := http.Header{}
|
||||
if settings.origin != "" {
|
||||
header.Set("Origin", settings.origin)
|
||||
}
|
||||
|
||||
conn, res, err := websocket.Dial(
|
||||
ctx,
|
||||
settings.url,
|
||||
&websocket.DialOptions{
|
||||
HTTPHeader: header,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if res != nil {
|
||||
return nil, fmt.Errorf("upgrade refused: %s", res.Status)
|
||||
}
|
||||
return nil, fmt.Errorf("dialling %s: %w", settings.url, err)
|
||||
}
|
||||
fmt.Fprintf(out, "connected (%s)\n", res.Status) //nolint:errcheck // printing only
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// pingPong sends a ping on conn and waits at most timeout for its pong,
|
||||
// printing each message received meanwhile to out.
|
||||
func pingPong(
|
||||
ctx context.Context,
|
||||
out io.Writer,
|
||||
conn *websocket.Conn,
|
||||
conn connection,
|
||||
timeout time.Duration,
|
||||
) error {
|
||||
ping, err := json.Marshal(hub.Envelope{
|
||||
@@ -312,7 +401,7 @@ func interact(
|
||||
ctx context.Context,
|
||||
input io.Reader,
|
||||
out io.Writer,
|
||||
conn *websocket.Conn,
|
||||
conn connection,
|
||||
timeout time.Duration,
|
||||
) error {
|
||||
listening := make(chan error, 1)
|
||||
@@ -366,7 +455,7 @@ func interact(
|
||||
// send writes message to conn as a text frame, waiting at most timeout.
|
||||
func send(
|
||||
ctx context.Context,
|
||||
conn *websocket.Conn,
|
||||
conn connection,
|
||||
message string,
|
||||
timeout time.Duration,
|
||||
) error {
|
||||
@@ -386,7 +475,7 @@ func send(
|
||||
func receive(
|
||||
ctx context.Context,
|
||||
out io.Writer,
|
||||
conn *websocket.Conn,
|
||||
conn connection,
|
||||
timeout time.Duration,
|
||||
) error {
|
||||
readCtx, cancel := util.WithTimeout(ctx, timeout)
|
||||
@@ -404,7 +493,7 @@ func receive(
|
||||
// listen prints each message received on conn to out until ctx is cancelled
|
||||
// (Ctrl+C) or the server closes the connection, either of which is a normal
|
||||
// end.
|
||||
func listen(ctx context.Context, out io.Writer, conn *websocket.Conn) error {
|
||||
func listen(ctx context.Context, out io.Writer, conn connection) error {
|
||||
for {
|
||||
_, data, err := conn.Read(ctx)
|
||||
status := websocket.CloseStatus(err)
|
||||
@@ -412,8 +501,12 @@ func listen(ctx context.Context, out io.Writer, conn *websocket.Conn) error {
|
||||
case err == nil:
|
||||
fmt.Fprintf(out, "< %s\n", data) //nolint:errcheck // printing only
|
||||
case ctx.Err() != nil:
|
||||
// Ctrl+C: Read has closed the connection already.
|
||||
// Ctrl+C: dial closes the connection.
|
||||
return nil //nolint:nilerr // a normal end
|
||||
case errors.Is(err, io.EOF):
|
||||
// A direct connection has no close status.
|
||||
fmt.Fprintln(out, "closed by the server") //nolint:errcheck // printing only
|
||||
return nil
|
||||
case status != -1:
|
||||
fmt.Fprintf(out, "closed by the server (%d)\n", status) //nolint:errcheck // printing only
|
||||
return nil
|
||||
|
||||
Reference in new issue
Block a user