From f55a0b50529cc005acf15c56165a9abf5455442d Mon Sep 17 00:00:00 2001 From: pgodwin Date: Sun, 19 Apr 2026 13:14:38 +1000 Subject: [PATCH] Focusing on ASP reliability. * Add comments to methods to make it more obvious which parts of the spec is implemented. * Checked the error codes against Inside Macintosh, handle invalid session ids gracefully. Previously clients would just get stuck in a loop and we'd just ignore them. * Return the correct error code if requests exceed quantum size. Previously we just silently truncated them. * double-check sizes and return correct error codes. * Get client attention when we stop the services so the client is informed of the shutdown. --- server.ini | 2 +- service/asp/asp.go | 199 +++++++++++++++++++++++++++++++++++----- service/asp/asp_test.go | 177 +++++++++++++++++++++++++++++++++++ service/asp/session.go | 11 +++ service/asp/types.go | 24 ++--- 5 files changed, 378 insertions(+), 35 deletions(-) create mode 100644 service/asp/asp_test.go diff --git a/server.ini b/server.ini index 117538b..35f338c 100644 --- a/server.ini +++ b/server.ini @@ -25,7 +25,7 @@ bridge_host_mac = ; optional host adapter MAC for Wi-Fi bridge sh [MacIP] ; MacIP Gateway Settings. Allows TCP over DDP. -enabled = false ; true to enable MacIP Gateway, false to disable +enabled = true ; true to enable MacIP Gateway, false to disable mode = pcap ; modes are pcap or nat. zone = ; MacIP Gateway Zone, defaults to EtherTalk zone, otherwise the first zone detected. nat_subnet = ; in NAT mode, the subnet to use (eg 192.168.100.0/24) diff --git a/service/asp/asp.go b/service/asp/asp.go index 0f576f2..98d6404 100644 --- a/service/asp/asp.go +++ b/service/asp/asp.go @@ -52,6 +52,14 @@ type Service struct { onSessionActivity func(*Session) } +// Spec-to-implementation mapping notes: +// - No separate SPGetSession method: session acceptance is handled inside +// handleOpenSession. +// - No separate SPGetRequest/SPCmdReply/SPWrtReply/SPWrtContinue methods: +// these are represented by handleCommand/handleASPWrite/completeWrite. +// - No separate SPNewStatus method: status is sourced from +// commandHandler.GetStatus() when servicing SPGetStatus. + // requestContext is what the host service threads through atp.HandleInbound // so the Sender bridge can use router.Reply on the way out. type requestContext struct { @@ -91,7 +99,13 @@ func (s *Service) SetCommandHandler(handler afp.CommandHandler) { // Socket returns the socket number this service listens on. func (s *Service) Socket() uint8 { return ServerSocket } -// Start performs SPGetParms/SPInit, registers NBP, and stands up the engine. +// Start performs server-side initialization corresponding to: +// - SPGetParms (server end; server ASP client -> ASP) +// - SPInit (server end; server ASP client -> ASP) +// +// In this implementation, SPInit is represented by wiring the SLS endpoint and +// validating ServiceStatusBlock size against QuantumSize before accepting +// traffic. func (s *Service) Start(router service.Router) error { s.router = router @@ -140,7 +154,14 @@ func (s *Service) registerInZone(zone []byte) { } // Stop unregisters NBP and shuts everything down. +// Before teardown, it sends a best-effort SPAttention(ServerGoingDown) to +// active sessions so workstation clients can terminate cleanly. func (s *Service) Stop() error { + for _, sessID := range s.sm.SessionIDs() { + if err := s.SendAttention(sessID, AspAttnServerGoingDown); err != nil { + netlog.Debug("[ASP] Stop: SendAttention failed for sess=%d: %v", sessID, err) + } + } for _, z := range s.registeredZones { s.nbp.UnregisterName([]byte(s.serverName), []byte(nbpType), z) } @@ -188,7 +209,11 @@ func (s *Service) sendBridge(src, dst atp.Address, payload []byte, hint any) err return s.router.Route(dg, true) } -// handleATPRequest is the atp.RequestHandler dispatched for every TReq. +// handleATPRequest is the server-side dispatcher for ASP network requests. +// Direction by SPFunction per spec: +// - workstation -> server: OpenSess, GetStatus, Command, Write, CloseSess +// - both directions: Tickle +// // It demultiplexes on the ASP function code in the user-data MSB. func (s *Service) handleATPRequest(in atp.IncomingRequest, reply atp.Replier) { aspCmd := uint8((in.UserBytes >> 24) & 0xFF) @@ -258,20 +283,43 @@ func (s *Service) chunkResponse(data []byte, bitmap uint8) [][]byte { return bufs } +// handleGetStatus implements SPGetStatus servicing on the server side +// (workstation ASP client -> server SLS). +// +// Related server-end calls from the spec: +// - SPInit provides initial ServiceStatusBlock. +// - SPNewStatus updates status for later SPGetStatus calls. +// +// In this code, status comes from commandHandler.GetStatus() at request time. func (s *Service) handleGetStatus(in atp.IncomingRequest, reply atp.Replier) { var status []byte if s.commandHandler != nil { status = s.commandHandler.GetStatus() } + if len(status) > s.effectiveQuantumSize() { + netlog.Info("[ASP] GetStatus: ServiceStatusBlockSize=%d exceeds QuantumSize=%d (SPErrorSizeErr)", + len(status), s.effectiveQuantumSize()) + reply(atp.ResponseMessage{ + Buffers: [][]byte{nil}, + UserBytes: []uint32{errToUserBytes(SPErrorSizeErr)}, + }) + return + } reply(atp.ResponseMessage{Buffers: s.chunkResponse(status, in.Bitmap)}) } +// handleOpenSession implements SPOpenSession handling at the server side +// (workstation ASP client -> server SLS). +// +// Spec note: classic ASP may gate acceptance on pending SPGetSession calls. +// This implementation models SPGetSession implicitly by accepting while session +// capacity is available. func (s *Service) handleOpenSession(in atp.IncomingRequest, reply atp.Replier) { pkt := ParseOpenSessPacket(in.UserBytes) if pkt.VersionNum != ASPVersion { netlog.Info("[ASP] OpenSess: bad version 0x%04X from %s", pkt.VersionNum, in.Src) - r := OpenSessReplyPacket{SSSSocket: ServerSocket, ErrorCode: aspBadVersNum} + r := OpenSessReplyPacket{SSSSocket: ServerSocket, ErrorCode: SPErrorBadVersNum} reply(atp.ResponseMessage{ Buffers: [][]byte{nil}, UserBytes: []uint32{r.MarshalUserData()}, @@ -281,7 +329,7 @@ func (s *Service) handleOpenSession(in atp.IncomingRequest, reply atp.Replier) { sess := s.sm.Open(in.Src.Net, in.Src.Node, pkt.WSSSocket, in.Local.Net, in.Local.Node) if sess == nil { - r := OpenSessReplyPacket{SSSSocket: ServerSocket, ErrorCode: aspTooManyClients} + r := OpenSessReplyPacket{SSSSocket: ServerSocket, ErrorCode: SPErrorTooManyClients} reply(atp.ResponseMessage{ Buffers: [][]byte{nil}, UserBytes: []uint32{r.MarshalUserData()}, @@ -292,15 +340,25 @@ func (s *Service) handleOpenSession(in atp.IncomingRequest, reply atp.Replier) { if s.onSessionOpen != nil { s.onSessionOpen(sess) } - r := OpenSessReplyPacket{SSSSocket: ServerSocket, SessionID: sess.ID, ErrorCode: aspNoErr} + r := OpenSessReplyPacket{SSSSocket: ServerSocket, SessionID: sess.ID, ErrorCode: SPErrorNoError} reply(atp.ResponseMessage{ Buffers: [][]byte{nil}, UserBytes: []uint32{r.MarshalUserData()}, }) } +// handleCloseSession handles CloseSess packets from workstation -> server and +// maps them to server-side SPCloseSession semantics. func (s *Service) handleCloseSession(in atp.IncomingRequest, reply atp.Replier) { pkt := ParseCloseSessPacket(in.UserBytes) + if s.sm.Get(pkt.SessionID) == nil { + netlog.Debug("[ASP] CloseSess: unknown SessRefNum=%d", pkt.SessionID) + reply(atp.ResponseMessage{ + Buffers: [][]byte{nil}, + UserBytes: []uint32{errToUserBytes(SPErrorParamErr)}, + }) + return + } s.sm.Close(pkt.SessionID) reply(atp.ResponseMessage{ Buffers: [][]byte{nil}, @@ -308,13 +366,31 @@ func (s *Service) handleCloseSession(in atp.IncomingRequest, reply atp.Replier) }) } +// handleCommand implements the SPCommand/SPCmdReply transaction path: +// 1. workstation -> server Command request +// 2. server -> workstation CmdReply result +// +// In classic server-end API terms, this combines SPGetRequest (Command type) +// and SPCmdReply. func (s *Service) handleCommand(in atp.IncomingRequest, reply atp.Replier) { receivedAt := time.Now() pkt := ParseCommandPacket(in.UserBytes, in.Data) + if len(pkt.CmdBlock) > s.effectiveMaxCmdSize() { + netlog.Debug("[ASP] Command: CmdBlockSize=%d exceeds MaxCmdSize=%d (SPErrorSizeErr)", + len(pkt.CmdBlock), s.effectiveMaxCmdSize()) + reply(atp.ResponseMessage{ + Buffers: [][]byte{nil}, + UserBytes: []uint32{errToUserBytes(SPErrorSizeErr)}, + }) + return + } sess := s.sm.Get(pkt.SessionID) if sess == nil { - netlog.Debug("[ASP] Command: unknown session %d", pkt.SessionID) - reply(atp.ResponseMessage{Buffers: [][]byte{nil}}) + netlog.Debug("[ASP] Command: unknown SessRefNum=%d", pkt.SessionID) + reply(atp.ResponseMessage{ + Buffers: [][]byte{nil}, + UserBytes: []uint32{errToUserBytes(SPErrorParamErr)}, + }) return } sess.touchActivity() @@ -336,10 +412,29 @@ func (s *Service) handleCommand(in atp.IncomingRequest, reply atp.Replier) { if s.commandHandler != nil { replyData, errCode = s.commandHandler.HandleCommand(pkt.CmdBlock) } + if len(replyData) > s.effectiveQuantumSize() { + netlog.Debug("[ASP] Command: SessRefNum=%d CmdReplyDataSize=%d exceeds QuantumSize=%d (SPErrorSizeErr)", + pkt.SessionID, len(replyData), s.effectiveQuantumSize()) + reply(atp.ResponseMessage{ + Buffers: [][]byte{nil}, + UserBytes: []uint32{errToUserBytes(SPErrorSizeErr)}, + }) + return + } + if wsCap := bitmapMaxBytes(in.Bitmap); wsCap > 0 && len(replyData) > wsCap { + netlog.Debug("[ASP] Command: reply %d exceeds workstation capacity %d (SPErrorBufTooSmall)", + len(replyData), wsCap) + bufs := s.chunkResponse(replyData, in.Bitmap) + reply(atp.ResponseMessage{ + Buffers: bufs, + UserBytes: []uint32{errToUserBytes(SPErrorBufTooSmall)}, + }) + return + } bufs := s.chunkResponse(replyData, in.Bitmap) reply(atp.ResponseMessage{ Buffers: bufs, - UserBytes: []uint32{uint32(errCode)}, + UserBytes: []uint32{errToUserBytes(errCode)}, }) elapsed := time.Since(receivedAt) replyBytes := 0 @@ -353,24 +448,36 @@ func (s *Service) handleCommand(in atp.IncomingRequest, reply atp.Replier) { } } -// handleASPWrite implements phase 1 of the two-phase ASP write protocol: +// handleASPWrite implements SPWrite handling (phase 1 of 2) on the server side: // -// 1. Workstation → server: Write TReq with the AFP command block. -// 2. Server → workstation WSS: WriteContinue TReq carrying the buffer size. -// 3. Workstation → server (TResp to WriteContinue): the actual write data. -// 4. Server → workstation: TResp to the original Write TReq with the AFP result. +// 1. workstation -> server: Write TReq with command block +// 2. server -> workstation: SPWrtContinue (WriteContinue TReq) +// 3. workstation -> server: WriteContinue TResp with write data +// 4. server -> workstation: SPWrtReply for the original Write TReq // // We capture `reply` from step 1 and invoke it in step 4 once the // WriteContinue Pending resolves with the data. +// +// In classic server-end API terms, this combines SPGetRequest (Write type) +// with SPWrtContinue and SPWrtReply. func (s *Service) handleASPWrite(in atp.IncomingRequest, reply atp.Replier) { receivedAt := time.Now() pkt := ParseWritePacket(in.UserBytes, in.Data) + if len(pkt.CmdBlock) > s.effectiveMaxCmdSize() { + netlog.Debug("[ASP] Write: CmdBlockSize=%d exceeds MaxCmdSize=%d (SPErrorSizeErr)", + len(pkt.CmdBlock), s.effectiveMaxCmdSize()) + reply(atp.ResponseMessage{ + Buffers: [][]byte{nil}, + UserBytes: []uint32{errToUserBytes(SPErrorSizeErr)}, + }) + return + } sess := s.sm.Get(pkt.SessionID) if sess == nil { - netlog.Debug("[ASP] Write: unknown session %d", pkt.SessionID) + netlog.Debug("[ASP] Write: unknown SessRefNum=%d", pkt.SessionID) reply(atp.ResponseMessage{ Buffers: [][]byte{nil}, - UserBytes: []uint32{errToUserBytes(aspParamErr)}, + UserBytes: []uint32{errToUserBytes(SPErrorParamErr)}, }) return } @@ -387,7 +494,17 @@ func (s *Service) handleASPWrite(in atp.IncomingRequest, reply atp.Replier) { var wantBytes uint32 if len(pkt.CmdBlock) >= 12 { - wantBytes = binary.BigEndian.Uint32(pkt.CmdBlock[8:12]) + rawWantBytes := int32(binary.BigEndian.Uint32(pkt.CmdBlock[8:12])) + if rawWantBytes < 0 { + netlog.Debug("[ASP] Write: negative BufferSize=%d in SPWrtContinue request metadata (SPErrorParamErr)", + rawWantBytes) + reply(atp.ResponseMessage{ + Buffers: [][]byte{nil}, + UserBytes: []uint32{errToUserBytes(SPErrorParamErr)}, + }) + return + } + wantBytes = uint32(rawWantBytes) } if max := uint32(s.quantumSize); wantBytes > max { netlog.Info("[ASP] Write sess=%d: clamping wantBytes %d→%d", @@ -431,7 +548,7 @@ func (s *Service) handleASPWrite(in atp.IncomingRequest, reply atp.Replier) { netlog.Debug("[ASP] Write sess=%d: WriteContinue SendRequest failed: %v", pkt.SessionID, err) reply(atp.ResponseMessage{ Buffers: [][]byte{nil}, - UserBytes: []uint32{errToUserBytes(aspParamErr)}, + UserBytes: []uint32{errToUserBytes(SPErrorParamErr)}, }) return } @@ -455,6 +572,8 @@ func (s *Service) handleASPWrite(in atp.IncomingRequest, reply atp.Replier) { go s.completeWrite(sess, pkt.CmdBlock, wantBytes, pending, reply, in.Bitmap, receivedAt, wcSentAt) } +// completeWrite finalizes the server-side SPWrite flow after SPWrtContinue has +// returned write data, then sends the SPWrtReply-equivalent result. func (s *Service) completeWrite(sess *Session, cmdBlock []byte, wantBytes uint32, pending *atp.Pending, reply atp.Replier, bitmap uint8, receivedAt, wcSentAt time.Time) { resp, err := pending.Wait(context.Background()) @@ -468,7 +587,7 @@ func (s *Service) completeWrite(sess *Session, cmdBlock []byte, wantBytes uint32 netlog.Debug("[ASP] Write sess=%d: WriteContinue failed after %v: %v", sess.ID, wcRTT.Round(time.Millisecond), err) reply(atp.ResponseMessage{ Buffers: [][]byte{nil}, - UserBytes: []uint32{errToUserBytes(aspParamErr)}, + UserBytes: []uint32{errToUserBytes(SPErrorParamErr)}, }) return } @@ -493,10 +612,29 @@ func (s *Service) completeWrite(sess *Session, cmdBlock []byte, wantBytes uint32 if s.commandHandler != nil { replyData, errCode = s.commandHandler.HandleCommand(full) } + if len(replyData) > s.effectiveQuantumSize() { + netlog.Debug("[ASP] Write: SessRefNum=%d WrtReplyDataSize=%d exceeds QuantumSize=%d (SPErrorSizeErr)", + sess.ID, len(replyData), s.effectiveQuantumSize()) + reply(atp.ResponseMessage{ + Buffers: [][]byte{nil}, + UserBytes: []uint32{errToUserBytes(SPErrorSizeErr)}, + }) + return + } + if wsCap := bitmapMaxBytes(bitmap); wsCap > 0 && len(replyData) > wsCap { + netlog.Debug("[ASP] Write: reply %d exceeds workstation capacity %d (SPErrorBufTooSmall)", + len(replyData), wsCap) + bufs := s.chunkResponse(replyData, bitmap) + reply(atp.ResponseMessage{ + Buffers: bufs, + UserBytes: []uint32{errToUserBytes(SPErrorBufTooSmall)}, + }) + return + } bufs := s.chunkResponse(replyData, bitmap) reply(atp.ResponseMessage{ Buffers: bufs, - UserBytes: []uint32{uint32(errCode)}, + UserBytes: []uint32{errToUserBytes(errCode)}, }) totalElapsed := time.Since(receivedAt) replyBytes := 0 @@ -536,19 +674,36 @@ func (s *Service) sendTickle(sess *Session) { // uint32 wire encoding without tripping Go's constant-overflow check. func errToUserBytes(code int32) uint32 { return uint32(code) } -// SPGetParms returns the maximum command block size and quantum size. +func (s *Service) effectiveQuantumSize() int { + if s.quantumSize > 0 { + return s.quantumSize + } + return QuantumSize +} + +func (s *Service) effectiveMaxCmdSize() int { + if s.maxCmdSize > 0 { + return s.maxCmdSize + } + return ATPMaxData +} + +// SPGetParms implements SPGetParms (both ends): ASP client -> ASP local query +// for MaxCmdSize and QuantumSize. func (s *Service) SPGetParms() GetParmsResult { return GetParmsResult{MaxCmdSize: ATPMaxData, QuantumSize: QuantumSize} } -// SendAttention sends an ASP Attention to the workstation end of a session. +// SendAttention implements server-side SPAttention +// (server ASP client -> workstation end of an open session). func (s *Service) SendAttention(sessID uint8, code uint16) error { if code == 0 { return fmt.Errorf("ASP: attention code must be non-zero") } sess := s.sm.Get(sessID) if sess == nil { - return fmt.Errorf("ASP: unknown session %d", sessID) + netlog.Debug("[ASP] Attention: unknown SessRefNum=%d", sessID) + return fmt.Errorf("ASP SPAttention: unknown SessRefNum=%d (SPErrorParamErr=%d)", sessID, SPErrorParamErr) } if s.endpoint == nil { return fmt.Errorf("ASP: not started") diff --git a/service/asp/asp_test.go b/service/asp/asp_test.go new file mode 100644 index 0000000..e196ec7 --- /dev/null +++ b/service/asp/asp_test.go @@ -0,0 +1,177 @@ +package asp + +import ( + "encoding/binary" + "testing" + + "github.com/pgodw/omnitalk/go/service/atp" +) + +type stubCommandHandler struct { + status []byte + reply []byte + err int32 +} + +func (h stubCommandHandler) HandleCommand(_ []byte) ([]byte, int32) { + return append([]byte(nil), h.reply...), h.err +} + +func (h stubCommandHandler) GetStatus() []byte { + return append([]byte(nil), h.status...) +} + +func TestHandleCommandUnknownSessionReturnsParamErr(t *testing.T) { + s := New("test", nil, nil, nil) + s.quantumSize = QuantumSize + + in := atp.IncomingRequest{ + UserBytes: (uint32(SPFuncCommand) << 24) | (uint32(42) << 16), + Data: []byte{0x01}, + Bitmap: 0x01, + } + + var got atp.ResponseMessage + s.handleCommand(in, func(m atp.ResponseMessage) { got = m }) + + if len(got.UserBytes) != 1 || got.UserBytes[0] != errToUserBytes(SPErrorParamErr) { + t.Fatalf("expected ParamErr user bytes, got %#v", got.UserBytes) + } +} + +func TestHandleCloseSessionUnknownSessionReturnsParamErr(t *testing.T) { + s := New("test", nil, nil, nil) + + in := atp.IncomingRequest{ + UserBytes: (uint32(SPFuncCloseSess) << 24) | (uint32(99) << 16), + } + + var got atp.ResponseMessage + s.handleCloseSession(in, func(m atp.ResponseMessage) { got = m }) + + if len(got.UserBytes) != 1 || got.UserBytes[0] != errToUserBytes(SPErrorParamErr) { + t.Fatalf("expected ParamErr user bytes, got %#v", got.UserBytes) + } +} + +func TestHandleCommandReplyOverQuantumReturnsSizeErr(t *testing.T) { + h := stubCommandHandler{reply: make([]byte, 12), err: SPErrorNoError} + s := New("test", h, nil, nil) + s.quantumSize = 8 + s.sm.Open(1, 1, 1, 1, 1) + + in := atp.IncomingRequest{ + UserBytes: (uint32(SPFuncCommand) << 24) | (uint32(1) << 16) | 1, + Data: []byte{0x01}, + Bitmap: 0xFF, + TID: 1, + } + + var got atp.ResponseMessage + s.handleCommand(in, func(m atp.ResponseMessage) { got = m }) + + if len(got.UserBytes) != 1 || got.UserBytes[0] != errToUserBytes(SPErrorSizeErr) { + t.Fatalf("expected SizeErr user bytes, got %#v", got.UserBytes) + } +} + +func TestHandleGetStatusOverQuantumReturnsSizeErr(t *testing.T) { + h := stubCommandHandler{status: make([]byte, 10)} + s := New("test", h, nil, nil) + s.quantumSize = 8 + + in := atp.IncomingRequest{Bitmap: 0xFF} + + var got atp.ResponseMessage + s.handleGetStatus(in, func(m atp.ResponseMessage) { got = m }) + + if len(got.UserBytes) != 1 || got.UserBytes[0] != errToUserBytes(SPErrorSizeErr) { + t.Fatalf("expected SizeErr user bytes, got %#v", got.UserBytes) + } +} + +func TestHandleCommandCmdBlockOverMaxReturnsSizeErr(t *testing.T) { + s := New("test", nil, nil, nil) + s.maxCmdSize = 4 + s.quantumSize = QuantumSize + + in := atp.IncomingRequest{ + UserBytes: (uint32(SPFuncCommand) << 24) | (uint32(1) << 16), + Data: []byte{1, 2, 3, 4, 5}, + Bitmap: 0x01, + } + + var got atp.ResponseMessage + s.handleCommand(in, func(m atp.ResponseMessage) { got = m }) + + if len(got.UserBytes) != 1 || got.UserBytes[0] != errToUserBytes(SPErrorSizeErr) { + t.Fatalf("expected SizeErr user bytes, got %#v", got.UserBytes) + } +} + +func TestHandleCommandReplyOverWorkstationCapacityReturnsBufTooSmall(t *testing.T) { + h := stubCommandHandler{reply: make([]byte, ATPMaxData+10), err: SPErrorNoError} + s := New("test", h, nil, nil) + s.maxCmdSize = ATPMaxData + s.quantumSize = QuantumSize + s.sm.Open(1, 1, 1, 1, 1) + + in := atp.IncomingRequest{ + UserBytes: (uint32(SPFuncCommand) << 24) | (uint32(1) << 16) | 1, + Data: []byte{0x01}, + Bitmap: 0x01, + TID: 1, + } + + var got atp.ResponseMessage + s.handleCommand(in, func(m atp.ResponseMessage) { got = m }) + + if len(got.UserBytes) != 1 || got.UserBytes[0] != errToUserBytes(SPErrorBufTooSmall) { + t.Fatalf("expected BufTooSmall user bytes, got %#v", got.UserBytes) + } +} + +func TestHandleWriteCmdBlockOverMaxReturnsSizeErr(t *testing.T) { + s := New("test", nil, nil, nil) + s.maxCmdSize = 4 + s.quantumSize = QuantumSize + + in := atp.IncomingRequest{ + UserBytes: (uint32(SPFuncWrite) << 24) | (uint32(1) << 16), + Data: []byte{1, 2, 3, 4, 5}, + Bitmap: 0x01, + } + + var got atp.ResponseMessage + s.handleASPWrite(in, func(m atp.ResponseMessage) { got = m }) + + if len(got.UserBytes) != 1 || got.UserBytes[0] != errToUserBytes(SPErrorSizeErr) { + t.Fatalf("expected SizeErr user bytes, got %#v", got.UserBytes) + } +} + +func TestHandleWriteNegativeBufferSizeReturnsParamErr(t *testing.T) { + s := New("test", nil, nil, nil) + s.maxCmdSize = ATPMaxData + s.quantumSize = QuantumSize + s.sm.Open(1, 1, 1, 1, 1) + + cmd := make([]byte, 12) + binary.BigEndian.PutUint32(cmd[8:12], uint32(0xFFFFFFFF)) + + in := atp.IncomingRequest{ + UserBytes: (uint32(SPFuncWrite) << 24) | (uint32(1) << 16) | 1, + Data: cmd, + Bitmap: 0x01, + TID: 1, + Src: atp.Address{Net: 1, Node: 1, Socket: 1}, + Local: atp.Address{Net: 1, Node: 2, Socket: ServerSocket}, + } + + var got atp.ResponseMessage + s.handleASPWrite(in, func(m atp.ResponseMessage) { got = m }) + + if len(got.UserBytes) != 1 || got.UserBytes[0] != errToUserBytes(SPErrorParamErr) { + t.Fatalf("expected ParamErr user bytes, got %#v", got.UserBytes) + } +} diff --git a/service/asp/session.go b/service/asp/session.go index e7c4b49..0f00177 100644 --- a/service/asp/session.go +++ b/service/asp/session.go @@ -141,6 +141,17 @@ func (m *SessionManager) Get(id uint8) *Session { return m.sessions[id] } +// SessionIDs returns a snapshot of currently active session IDs. +func (m *SessionManager) SessionIDs() []uint8 { + m.mu.RLock() + defer m.mu.RUnlock() + ids := make([]uint8, 0, len(m.sessions)) + for id := range m.sessions { + ids = append(ids, id) + } + return ids +} + // Close terminates a session. func (m *SessionManager) Close(id uint8) { m.mu.Lock() diff --git a/service/asp/types.go b/service/asp/types.go index a9bc8e0..05cd0a0 100644 --- a/service/asp/types.go +++ b/service/asp/types.go @@ -47,17 +47,17 @@ const ( // --------------------------------------------------------------------------- const ( - aspNoErr = 0 // $00 — no error (both ends) - aspBadVersNum = -1066 // $FBD6 — workstation end only - aspBufTooSmall = -1067 // $FBD5 — workstation end only - aspNoMoreSess = -1068 // $FBD4 — both ends - aspNoServers = -1069 // $FBD3 — workstation end only - aspParamErr = -1070 // $FBD2 — both ends - aspServerBusy = -1071 // $FBD1 — workstation end only - aspSessClosed = -1072 // $FBD0 — both ends - aspSizeErr = -1073 // $FBCF — both ends - aspTooManyClients = -1074 // $FBCE — server end only - aspNoAck = -1075 // $FBCD — server end only + SPErrorNoError = 0 // $00 — no error (both ends) + SPErrorBadVersNum = -1066 // $FBD6 — workstation end only + SPErrorBufTooSmall = -1067 // $FBD5 — workstation end only + SPErrorNoMoreSessions = -1068 // $FBD4 — both ends + SPErrorNoServers = -1069 // $FBD3 — workstation end only + SPErrorParamErr = -1070 // $FBD2 — both ends + SPErrorServerBusy = -1071 // $FBD1 — workstation end only + SPErrorSessClosed = -1072 // $FBD0 — both ends + SPErrorSizeErr = -1073 // $FBCF — both ends + SPErrorTooManyClients = -1074 // $FBCE — server end only + SPErrorNoAck = -1075 // $FBCD — server end only ) // AFP attention codes sent via SPFuncAttention. @@ -147,7 +147,7 @@ func ParseOpenSessPacket(userData uint32) OpenSessPacket { type OpenSessReplyPacket struct { SSSSocket uint8 // server session socket SessionID uint8 - ErrorCode int16 // 0 = success; aspBadVersNum, aspServerBusy, aspTooManyClients + ErrorCode int16 // 0 = success; SPErrorBadVersNum, SPErrorServerBusy, SPErrorTooManyClients } // MarshalUserData encodes the reply into the 4-byte ATP UserData field.