Repository navigation
Fake Server
Package fakeserver (doc.go). A server listens on
127.0.0.1:0 unless Options.Addr names another address.
s := fakeserver.New(t, fakeserver.Options{...}) // closed when the test ends
s, err := fakeserver.Start(fakeserver.Options{...}) // without testing; call s.Close()Options.Script is a list of steps. Every connection runs them in order,
after the greeting. Expect is an anchored regular expression; ""
matches any line.
s := fakeserver.New(t, fakeserver.Options{
Script: []fakeserver.Step{
{Expect: `MODE READER`, Reply: []string{"200 reader"}},
{Expect: `GROUP \S+`, Reply: []string{"211 3 1 3 alt.test"}},
{Expect: `XOVER .*`, Hang: true}, // never answers
},
})| Step field | Effect |
|---|---|
Expect |
anchored regexp for the client line |
ReadDot |
after the match, read a dot-terminated block (a POST or TAKETHIS payload) |
Delay |
wait before the reply |
Reply |
the reply lines (CRLF is added) |
Hang |
never reply again; the connection is held until the server closes |
CloseAfter |
close the connection after the reply |
Compress |
after the reply (e.g. 206 Compression active), compress the connection (COMPRESS DEFLATE) |
A line that doesn't match the current step gets
500 fakeserver: unexpected <line> and goes into s.Mismatches(). After
the last step the server keeps reading and recording lines until the client
closes.
Options.Handler gets each connection and does what it wants. The
connection is closed when the function returns.
s := fakeserver.New(t, fakeserver.Options{Handler: func(c *fakeserver.Conn) {
for {
line, err := c.ReadLine()
if err != nil {
return
}
if strings.EqualFold(line, "QUIT") {
c.WriteLines("205 bye")
return
}
c.WriteLines("500 what?")
}
}})Conn has ReadLine, WriteLines, ReadDotBlock, Raw (the
net.Conn), Server and ID (the accept order, from 1), and
StartCompress, StartCompressOutputOnly and Compressed (see
COMPRESS DEFLATE). Faults don't apply to your own
handler.
Options.Store serves a Store of groups and articles with a built-in
RFC 3977 reader. Options.StoreOpts changes how it behaves. See
Store and Groups and NNTP Commands.
-
RawStallwins over everything: the server accepts TCP and never writes (a TLS handshake or SOCKS stall). - Otherwise the greeting is written, unless
NoGreetingis set. ThenHandlerruns when set, elseStore, elseScript. - With
NoGreetingand no Handler, neither Store nor Script runs: lines are only recorded (a greeting stall).
| Field | Effect |
|---|---|
Addr |
listen address, default 127.0.0.1:0
|
Greeting |
default 200 fakeserver ready (201 ... for a ReaderOnly store) |
NoGreeting, RawStall
|
stalls, see above |
TLS |
a *tls.Config: TLS on every connection |
Script, Handler, Store, StoreOpts
|
the mode |
Faults |
see Faults |
Deadline |
bounds waits and delays; 0 = the hang guard (see Getting Started) |
MaxConns |
more than this many open connections: the next one gets MaxConnsGreeting and is closed |
MaxConnsGreeting |
default 502 Too many connections
|
| Method | Returns |
|---|---|
Addr, Host, Port
|
the listen address |
Lines() |
every client line, in order, all connections |
Articles() |
every dot block read (POST and TAKETHIS payloads, ReadDot steps), raw wire lines |
Mismatches() |
Script mode: lines no step matched |
Count(cmd), Counts()
|
how often each command was seen (the upper-cased first word of each line) |
Accepted(), Open()
|
connections accepted so far, and still open |
WaitOpen(t, n, d) |
wait until Open() == n
|
WaitLines(t, want) |
wait until Lines() equals want
|
Lines inside a dot block (an article) are not counted as commands.
TLSConfig makes a self-signed certificate for 127.0.0.1 and
localhost, and a pool that trusts it:
srvConf, roots := fakeserver.TLSConfig(t)
s := fakeserver.New(t, fakeserver.Options{Store: st, TLS: srvConf})
conn, err := tls.Dial("tcp", s.Addr(), &tls.Config{RootCAs: roots})
if err != nil {
t.Fatal(err)
}
c := client.New(t, conn)
if _, err := c.ReadGreeting(); err != nil {
t.Fatal(err)
}For a server under test that should reject the certificate, don't give it the pool. The kit has no STARTTLS.
RFC 8054 compression, opt-in:
| Where | How |
|---|---|
| Store server | StoreOpts.Compress: true |
| Streaming peer | PeerOptions.Compress: true |
| Script | a step with Reply: []string{"206 Compression active"}, Compress: true
|
| Your Handler | write the 206, then call c.StartCompress()
|
From the end of the 206 line on, both directions are compressed. Every
write (each reply, every fault, c.Raw().Write) is deflated and
sync-flushed. The client's stream is inflated as it arrives, whatever flush
the client uses, so Lines(), Counts(), Articles() and Mismatches()
record the commands as the client wrote them. Corrupt compressed input
closes the connection, as the RFC says. All Faults work on a compressed
connection.
The Store server and the peer:
| Command | Reply |
|---|---|
CAPABILITIES |
lists COMPRESS DEFLATE while compression is off (after the login, when one is required), not once it is on |
COMPRESS DEFLATE |
206 Compression active |
COMPRESS without or with a bad algorithm name (names are upper case, so deflate is bad) |
501 |
COMPRESS SHRINK (another algorithm) |
503 Compression algorithm not supported |
COMPRESS with any valid name while compression is on |
502 Compression already active |
COMPRESS before a required login |
480 |
AUTHINFO once compression is on |
502 |
The Store server starts compression when the status line it wrote for
COMPRESS, after the faults, is a 206. So Faults.Override
"COMPRESS": {"403 Unable to activate compression"} is a server that
cannot compress, and {} one that never answers.
StoreOpts.CompressOutputOnly (or PeerOptions.CompressOutputOnly, or
c.StartCompressOutputOnly() in a Handler) offers COMPRESS, answers 206
and compresses what it writes, but reads the client's bytes without
inflating them, as a server that only ever compresses its responses. A
client that compresses its commands, as RFC 8054 requires, gets no answer
(its compressed bytes rarely hold a line end) or 500 for a binary line;
the strict client shows that as a timeout or a 500 to its command.
Bytes a client pipelines behind COMPRESS DEFLATE (the RFC forbids that)
are inflated too, so plain text there is corrupt input and closes the
connection. INN's nnrpd reads such bytes as plain commands instead.
- Store mode:
StoreOpts.UserandPassrequireAUTHINFO USER/PASS; every other command but CAPABILITIES, MODE, HELP, DATE and QUIT gets480before it. A480written later ends the login, like a login that timed out (see Faults). - Script mode: script the AUTHINFO lines like any others.
- Streaming peer:
PeerOptions.UserandPass. CHECK, TAKETHIS and IHAVE get480before the login (TAKETHIS after its article, IHAVE at once, without a335).
PeerHandler answers like a transit peer that is fed articles, as innd
is: CAPABILITIES (VERSION 2, IHAVE, READER, STREAMING), MODE STREAM, MODE
READER, AUTHINFO, CHECK and TAKETHIS (RFC 4644), IHAVE (RFC 3977 6.3.2) and
QUIT. Anything else gets 500.
peer := &fakeserver.PeerOptions{
Have: map[string]bool{"<old@example.invalid>": true},
Defer431Once: map[string]bool{"<busy@example.invalid>": true},
}
s := fakeserver.New(t, fakeserver.Options{Handler: fakeserver.PeerHandler(peer)})
c := client.Dial(t, s.Addr())
c.Cmd("MODE STREAM") // 203
c.Cmd("CHECK <old@example.invalid>") // 438
c.Cmd("CHECK <busy@example.invalid>") // 431, then 238 the next time| PeerOptions field | Effect |
|---|---|
User, Pass
|
require AUTHINFO; CHECK, TAKETHIS and IHAVE get 480 before it |
Have |
message-IDs the peer has (CHECK 438, TAKETHIS 439, IHAVE 435); a TAKETHIS that gets 239 or an IHAVE that gets 235 adds its ID |
Defer431Once |
CHECK of these IDs gets 431 the first time |
Defer436Once |
IHAVE of these IDs gets 436 the first time, as the first reply: no article is sent |
Reject |
these articles are refused once they are read, every time, as a content filter does: CHECK 238, TAKETHIS 439, IHAVE 335 then 437. They are not added to Have and OnArticle is not called |
NoStream |
a peer without streaming: MODE STREAM gets 501, CHECK and TAKETHIS 500, CAPABILITIES has no STREAMING |
StallModeStream |
wait this long before the first MODE STREAM reply |
OnArticle |
called with each accepted article (raw, still dot-stuffed lines), before the 239 or 235 |
Compress, CompressOutputOnly
|
COMPRESS DEFLATE as in the Store server (see COMPRESS DEFLATE); AUTHINFO gets 502 once it is on. innd has no COMPRESS, INN's nnrpd has |
The handler keeps Have and the 431 and 436 deferrals across the
connections of a server. Read Have from the test only after s.Close().
IHAVE works with and without MODE STREAM, the way innd answers it:
| Situation | Reply |
|---|---|
| not logged in |
480, at once (the client sends no article) |
the ID is not <...> without spaces |
435 (innd's answer; RFC 3977 would allow 501) |
| the peer has the ID | 435 Duplicate |
the ID is in Defer436Once, first offer |
436 Retry later |
| otherwise |
335 Send it; after the article 235, or 437 when the ID is in Reject (or another connection delivered it meanwhile) |
innd sends 436 before the transfer when it is paused or the article is
arriving on another connection, and 437 only after it has read the
article; the peer does the same. Every IHAVE article is recorded in
s.Articles().
peer := &fakeserver.PeerOptions{
Have: map[string]bool{"<old@example.invalid>": true},
Reject: map[string]bool{"<spam@example.invalid>": true},
}
s := fakeserver.New(t, fakeserver.Options{Handler: fakeserver.PeerHandler(peer)})
c := client.Dial(t, s.Addr())
c.Cmd("IHAVE <old@example.invalid>") // 435
c.Cmd("IHAVE <new@example.invalid>") // 335
c.Send("Message-ID: <new@example.invalid>\r\n\r\nbody\r\n.\r\n")
c.ReadStatus() // 235innfeed and innxmit send MODE STREAM first and fall back to IHAVE when the
peer refuses it. NoStream: true makes the peer refuse it as innd with
streaming turned off does: MODE STREAM gets 501 Unknown MODE variant,
CAPABILITIES lists no STREAMING, and CHECK and TAKETHIS are unknown
commands (500). The peer doesn't read an article after such a TAKETHIS:
its lines are read as commands and each gets 500, as on innd. IHAVE
works as usual.
nntp-testkit is free software under the GNU General Public License, version 2 — see LICENSE.