Documentation
¶
Overview ¶
Package janus is a pure-Go implementation of the JANUS bidirectional file transfer protocol. See SPEC.md for the wire-format details.
Index ¶
Constants ¶
const ( JCAP_CRC32 byte = 0x80 // sender can encode CRC-32 on non-FNAMEPKT frames JCAP_FREQ byte = 0x40 // sender supports file requests JCAP_CLEAR8 byte = 0x20 // reserved; not implemented anywhere JCAP_KNOWN = JCAP_CRC32 | JCAP_FREQ | JCAP_CLEAR8 )
Capability flags carried in the trailing byte of FNAMEPKT (and optionally FNACKPKT). Mirrors qico janus.h:54-57.
const ( DLE byte = 0x10 XON byte = 0x11 XOFF byte = 0x13 CR byte = 0x0D AT byte = 0x40 )
DLE escape framing bytes.
const BufMax = 2048
BufMax is the maximum per-packet data payload (qico BUFMAX). BLKPKT carries a 4-byte offset prefix plus up to BufMax data bytes — a fully populated BLKPKT payload is therefore 4 + BufMax = 2052 bytes.
const SafeMaxOffset int64 = 0x7FFFFFFE
SafeMaxOffset is the largest file size or resume offset the protocol can safely carry on the wire. qico's TX-side resume check is signed ((txpos = FETCH32(rxbuf)) > -1L), so any value with the top bit set is interpreted as the "skip" sentinel rather than a real offset.
Variables ¶
var ( // ErrSkip is returned by RecvHandler.AcceptFile to refuse a single // incoming file. The library answers FNACKPKT(-1) on the wire; the // batch continues with the next FNAMEPKT. ErrSkip = errors.New("janus: skip file") // ErrRefuseBatch is returned by RecvHandler.AcceptFile to refuse the // offered file AND terminate the whole session, non-destructively // for the peer's queue: the library sends NO FNACKPKT for the // refused file (FNACKPKT(-1) would mean "skip — count it handled" // and the peer would clear the file), it just initiates the HALT // shutdown. The peer's in-flight offer then ends via its abort // teardown — aborted and requeued for a later session. Run returns // ErrSessionAborted. Use this instead of calling Session.Abort from // inside AcceptFile so the library controls the shutdown sequence. ErrRefuseBatch = errors.New("janus: refuse file and abort batch") // ErrTransportNotClosable is returned by Session.Run when neither the // transport implements io.Closer nor Config.Closer is supplied. // Returned before any goroutine is spawned and before the transport // is touched, so the caller still owns the transport. ErrTransportNotClosable = errors.New("janus: transport is not closable") // ErrFileTooLarge is delivered to FileCompleted (on whichever side // rejected the file) when a FileOffer.Size or announced incoming size // exceeds Config.MaxFileSize (default 0x7FFFFFFE). The file is never // transferred on the wire. ErrFileTooLarge = errors.New("janus: file size exceeds 0x7FFFFFFE") // ErrResumeOutOfRange is delivered to FileCompleted when a resume // offset falls outside [0, 0x7FFFFFFE] (RecvHandler.AcceptFile // returned one, or the peer requested one). The library answers // FNACKPKT(-1) on the wire; the batch continues. ErrResumeOutOfRange = errors.New("janus: resume offset outside [0, 0x7FFFFFFE]") // ErrHandlerContract is delivered to FileCompleted when a handler // violates its contract — RecvHandler.AcceptFile returned // (nil, _, nil), or a FileOffer carried a nil Reader. The library // answers FNACKPKT(-1) on the wire (receive side) or drops the offer // locally (send side); the batch continues. ErrHandlerContract = errors.New("janus: handler returned nil writer with nil error") // ErrBrainDead is returned by Run when the brain_dead watchdog // expires (no protocol forward progress for Config.BrainDead). ErrBrainDead = errors.New("janus: brain_dead timeout (no progress)") // ErrSessionAborted is returned by Run after Abort. ErrSessionAborted = errors.New("janus: session aborted") // ErrPeerHalt is the sentinel for peer-initiated HALTPKT. The // library no longer records it automatically (peer-initiated HALT // is the normal shutdown path), but it remains exposed for callers // that wish to surface the cause themselves via the Session API. ErrPeerHalt = errors.New("janus: peer halted") // ErrMaxRetries is delivered to SendHandler.FileCompleted when a // TX-side retransmission of FNAMEPKT or BLKPKT exceeds // Config.MaxRetries with no acknowledgement. ErrMaxRetries = errors.New("janus: retransmit limit exceeded") // ErrFreqHandlerRequired is returned by Session.Run at startup when // Config.SupportFreq is true but no FreqHandler was supplied to // NewSession. Advertising JCAP_FREQ without a way to satisfy or // originate requests is a configuration error. ErrFreqHandlerRequired = errors.New("janus: Config.SupportFreq=true requires a non-nil FreqHandler") )
Public sentinel errors. See PLAN.md "Public sentinels".
Functions ¶
func SanitizeFilename ¶
SanitizeFilename strips path components from a peer-provided filename so it is safe to pass to os.Create or os.OpenFile in the current working directory. Behaviour:
- filepath.Base on the raw name (strips any directory path)
- leading dots and slashes are removed
- if the result is "" or "." or "..", returns "_"
- NUL bytes and path separators inside the base are replaced with "_"
This is intentionally aggressive — applications that need richer destination-path policy should ignore this helper and implement their own.
Types ¶
type Config ¶
type Config struct {
// Originator is true on the side that initiated the connection. Used
// as a tiebreaker for the 5-consecutive-RPOS TX inhibit (BinkleyTerm
// behaviour layered on top of qico's wire spec — SPEC §14.3).
Originator bool
// EffectiveBaud feeds the timer/blocksize defaults. 0 means "treat
// as a fast link" — use BUFMAX and the 10 s timeout floor.
EffectiveBaud int
// MaxBlockSize caps the per-block payload. Default BufMax (2048).
MaxBlockSize int
// MinBlockSize is the floor after RPOS backoff. Default 128 (qico
// value).
MinBlockSize int
// DisableCRC32 opts out of advertising JCAP_CRC32. CRC-32 is enabled
// by default — set this true to fall back to CRC-16 for non-FNAMEPKT
// frames (useful for testing or for talking to a peer that mis-handles
// CRC-32 negotiation).
DisableCRC32 bool
// SupportFreq advertises JCAP_FREQ. Default false — FREQ is
// recognised on the wire but not driven from the library in v1.
SupportFreq bool
// Timeout governs TX ack waits. Default max(10s, 40960/baud).
Timeout time.Duration
// BrainDead is the no-progress watchdog. Default 120s. Resets only
// on TX successful XSENDBLK or RX good BLKPKT at the expected
// offset (qico janus.c:166, 228).
BrainDead time.Duration
// MaxRetries caps consecutive TX retransmits (FNAMEPKT after a lost
// FNACK, or the last BLKPKT after a lost EOFACK) before the file
// is failed with ErrMaxRetries and the session aborts. Default 10.
MaxRetries int
// MaxFileSize is the largest file the library is willing to send.
// Default SafeMaxOffset (0x7FFFFFFE). Larger offers are skipped at
// TX-time with ErrFileTooLarge.
MaxFileSize int64
// Closer, if non-nil, is closed during Run shutdown to unblock the
// reader goroutine on transports whose Read cannot otherwise be
// interrupted. If nil, the library type-asserts the transport for
// io.Closer; if neither is reachable, Run returns
// ErrTransportNotClosable at startup.
Closer io.Closer
}
Config controls Session behaviour. nil is acceptable and selects defaults.
type FileOffer ¶
type FileOffer struct {
Name string
Size int64
ModTime time.Time
Mode uint32
Reader io.Reader
// Close, if non-nil, releases the resources backing Reader. The
// library invokes it exactly once per offer handed to it, on every
// path: after the offer's FileCompleted when the transfer started or
// was rejected, or during session teardown for offers that never
// reached the wire (e.g. FREQ-resolved offers still queued at abort).
Close func() error
}
FileOffer describes one file the sender wants to push to the peer. Reader provides the file data. If Reader also implements io.ReadSeeker, the library can honour a non-zero resume offset returned by the peer's AcceptFile; otherwise a non-zero resume causes the file to be skipped.
type FnackPkt ¶
FnackPkt is the FNACKPKT body: 4-byte LE signed offset (interpreted via uint32 → int64), optionally followed by a 1-byte caps echo (present iff payload length ≥ 5). Offset 0xFFFFFFFF means "skip this file".
type FnamePkt ¶
type FnamePkt struct {
Name string
Size int64
Mtime int64 // Unix seconds (octal on the wire)
Mode uint32
Caps byte
}
FnamePkt is the parsed FNAMEPKT body: filename + size/mtime/mode + caps. An empty name signals end-of-batch.
type FreqHandler ¶
type FreqHandler interface {
// NextRequest returns the next filename the local side wants to
// request from the peer, or "" when no more requests are pending.
// The call is destructive — the library treats each non-empty return
// value as one request attempt and will not call NextRequest again
// until that attempt has resolved (peer sent a non-EOB FNAMEPKT in
// response) or been denied (peer sent FREQNAKPKT). FREQPKT
// retransmits caused by peer EOB retransmits are handled internally
// and do NOT trigger additional NextRequest calls.
NextRequest() string
// ResolveRequest is called when the peer sends FREQPKT. Return zero
// or more FileOffers to satisfy the request; the library will
// transmit each as a regular file in the local TX batch. Returning
// a nil or empty slice causes the library to NAK with FREQNAKPKT.
// The name argument is the peer's raw request string — wildcard or
// magic-alias resolution is the handler's concern.
ResolveRequest(name string) []*FileOffer
// RequestDenied is fired when the peer responds to our outgoing
// request with FREQNAKPKT. There is no positive-ack callback:
// successful responses arrive as ordinary AcceptFile invocations,
// and the protocol provides no wire-level way to correlate which
// incoming file (or files) satisfy which request.
RequestDenied(name string)
}
FreqHandler enables FREQPKT (file-request) support. It is supplied to NewSession as its own argument; Session.Run returns ErrFreqHandlerRequired when Config.SupportFreq is true but the FreqHandler is nil. Callbacks for FREQ-resolved outbound files flow through the SendHandler like any other offer.
type FreqPkt ¶
FreqPkt is the FREQPKT body: requested filename + caps byte. Spec: payload is "request_name\0caps".
type RecvHandler ¶
type RecvHandler interface {
// AcceptFile is invoked for every incoming file. The handler returns
// (writer, offset, nil) -- accept, resume at offset
// (nil, 0, ErrSkip) -- refuse this one file
// (nil, 0, ErrRefuseBatch) -- refuse file AND end session
// (_, _, err) -- refuse with a custom error
//
// On any non-nil error the library answers FNACKPKT(-1) on the wire;
// with ErrRefuseBatch it then initiates the HALT shutdown, otherwise
// the batch continues. Custom errors are surfaced verbatim via
// FileCompleted.
//
// SECURITY: incoming filenames are untrusted. Call SanitizeFilename
// before using info.Name as a path component.
AcceptFile(info FileInfo) (io.WriteCloser, int64, error)
// FileProgress is called periodically during transfer with the
// current byte count. The library makes no guarantee about cadence.
FileProgress(info FileInfo, bytesTransferred int64)
// FileCompleted is invoked exactly once per incoming file that was
// offered, when its transfer ends — on every path, including session
// teardown while the file was mid-receive (the writer is closed
// first; err is then ErrSessionAborted or the fatal error). err is
// nil on success.
FileCompleted(info FileInfo, bytesTransferred int64, err error)
}
RecvHandler is the application callback interface for the RX half of a session. All three methods are invoked from the session's RX goroutine. A nil RecvHandler at NewSession means every incoming file is skipped with FNACKPKT(-1). NOTE: unlike a non-destructive defer, a Janus skip tells the peer not to retry — the peer will treat skipped files as handled.
type RposPkt ¶
RposPkt is the RPOSPKT body: expected offset + Unix timestamp (used for dedup — see SPEC §4.4).
type SendHandler ¶
type SendHandler interface {
// NextFile returns the next file to send, or nil when the local TX
// batch is finished. It will be called repeatedly until it returns
// nil — at that point the library emits an end-of-batch FNAMEPKT.
NextFile() *FileOffer
// FileProgress is called periodically during transfer with the
// current byte count. The library makes no guarantee about cadence.
FileProgress(info FileInfo, bytesTransferred int64)
// FileCompleted is invoked exactly once per outbound file when its
// transfer ends — on every path, including session teardown while
// the file was mid-flight (err is then ErrSessionAborted or the
// fatal error). err is nil on success.
FileCompleted(info FileInfo, bytesTransferred int64, err error)
}
SendHandler is the application callback interface for the TX half of a session. All three methods are invoked from the session's TX goroutine. A nil SendHandler at NewSession means "nothing to send" — the library emits an end-of-batch FNAMEPKT immediately (FREQ-resolved offers, which also flow through TX, then have their callbacks dropped).
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is one bidirectional JANUS conversation. Create via NewSession, drive via Run, optionally interrupt via Abort.
func NewSession ¶
func NewSession(transport io.ReadWriter, send SendHandler, recv RecvHandler, freq FreqHandler, cfg *Config) *Session
NewSession constructs a Session bound to the given transport and handlers. send drives the TX half (nil = nothing to offer), recv drives the RX half (nil = skip all incoming files), freq serves FREQPKT file requests (nil unless Config.SupportFreq). cfg may be nil — defaults are then applied.
The transport is owned by the session for the lifetime of Run. After Run returns (with any error other than ErrTransportNotClosable or ErrFreqHandlerRequired), the transport has been closed and must not be reused.
func (*Session) Abort ¶
Abort initiates a clean shutdown by sending HALTPKT and tearing down. It is safe to call from any goroutine, including handler callbacks. Returns nil; the asynchronous Run will return ErrSessionAborted (or the underlying error if shutdown already started).