; CPC/IP telnet client ; Copyright (c) 1999-2001 Mark RISON ; See RFC854 ; See RFC857 for the echo option ; See RFC858 for the suppress go ahead option .telnet_syntax call printstringimm defb "telnet [-l] host [port]", CR, LF, 0 jp keyb_client_done TCP_TELNET equ 23 TELNET_BUFSIZ equ 257 ; Should be sizeof (keybbuffer) + 1 .telnet_init call printstringimm defb "telnet client initialised", CR, LF, 0 ret .telnet_du ; Check for absence of arguments call util_skipspace ld a, (hl) or a jr z, telnet_syntax ; Check for switch xor a ld (telnet_linemode), a ld a, (hl) cp SWITCH jr nz, telnet_noswitch inc hl ld a, (hl) cp 'l' jr nz, telnet_syntax ld (telnet_linemode), a inc hl call util_skipspace .telnet_noswitch ; Get the host address push hl call util_skiptospace ; Get the port, if supplied call util_skipspace ld a, (hl) or a ex de, hl ld hl, TCP_TELNET call nz, util_readdec2 ld (telnet_port), hl ; Register key handler (for CTRL-[ to abort resolving) ld hl, telnet_keyhandler_resolv call keyb_client_doing ; Look up the address using the resolver call printstringimm defb "[Abort character is '^[']", CR, LF, 0 pop hl ld de, telnet_resolved jp resolv_gethostbyname .telnet_resolved ; Check what the status code says or a ; DNS_NOERR jp nz, resolv_whine ; Resolved OK! ld de, telnet_active_bind + UCP_OFFSETOF_REMADDR ld bc, 4 ldir ; Register key handler (for CTRL-[ to abort session and for other input chars) ; or line handler (same, but for line mode) ld a, (telnet_linemode) or a jr z, telnet_resolved_charmode ld hl, telnet_linehandler call keyb_line_doing jr telnet_resolved_waslinemode .telnet_resolved_charmode ld hl, telnet_keyhandler call keyb_client_doing .telnet_resolved_waslinemode ; Confirm details and store the port call printstringimm defb "[", 0 ld hl, telnet_active_bind + UCP_OFFSETOF_REMADDR call printipaddr call printstringimm defb ":", 0 ld hl, (telnet_port) ld d, l ; Reverse the endianness ld e, h ld (telnet_active_bind + UCP_OFFSETOF_REMPORT), de call printdec2 call printstringimm defb "]", CR, LF, 0 ; Initialise state xor a ld (telnet_inIAC), a ld (telnet_inOPT), a ld (telnet_prevchar), a ld a, TELNET_WONT ld (telnet_remecho), a ld (telnet_remsga), a ld hl, 0 ld (telnet_active_bind + UCP_OFFSETOF_EXTRA + TCP_OFFSETOF_SNDNUN), hl call cons_init ; Choose random local port for TCP call ip_getranduserport ld (telnet_active_bind + UCP_OFFSETOF_LCLPORT), hl ; Open TCP socket ld hl, telnet_active_bind call tcp_connect or a ; Remember the resolver? ret ; Make sure it's thanked but turned away .telnet_active_bind defs 2 ; Linked-list link defs 4 ; Remote address defb 0, TCP_TELNET ; Remote port defs 2 ; Local port defs 2 ; TCP state handler defs 1 ; TCP state defw 0 ; Connection request upcall defw telnet_receive ; Receive upcall defw telnet_sent ; Sent upcall defw telnet_connclosed ; Connection closed upcall defs 4 ; SNDUNA defs 4 ; RCVNXT defw 256 ; RCVWND defs SIZEOF_TIMEOUT ; TCP retransmission timeout defw 0 ; SNDNUN defs TELNET_BUFSIZ ; SNDBUF TELNET_EOR equ 239 ; See RFC885 TELNET_SE equ 240 ; FIXME RFC1123 (3.2.3) says must support TELNET_NOP equ 241 ; FIXME RFC1123 (3.2.3) says must support TELNET_DM equ 242 ; FIXME RFC1123 (3.2.3) says must support TELNET_BRK equ 243 TELNET_IP equ 244 ; FIXME RFC1123 (3.2.3/3.4.2) says must support TELNET_AO equ 245 ; FIXME RFC1123 (3.2.3/3.4.2) says must support TELNET_AYT equ 246 ; FIXME RFC1123 (3.2.3/3.4.2) says must support TELNET_EC equ 247 TELNET_EL equ 248 TELNET_GA equ 249 TELNET_SB equ 250 ; FIXME RFC1123 (3.2.3) says must support TELNET_WILL equ 251 TELNET_WONT equ 252 TELNET_DO equ 253 TELNET_DONT equ 254 TELNET_IAC equ 255 TELNET_OPT_BIN equ 0 ; FIXME RFC1123 (3.3.3) says must support TELNET_OPT_ECHO equ 1 TELNET_OPT_SGA equ 3 ; FIXME RFC1123 (3.2.2/3.3.3) says must support TELNET_OPT_STAT equ 5 TELNET_OPT_TIME equ 6 ; FIXME but what the hell is that supposed TELNET_OPT_TERM equ 24 ; FIXME to mean, exactly? TELNET_OPT_NAWS equ 31 TELNET_OPT_EXT equ 255 .telnet_sent ld a, d or a ret nz .telnet_sent_connected call printstringimm defb "[Connected]", CR, LF, 0 ret .telnet_receive ; FIXME RFC1123 (3.2.4) says must discard if urgent and not command call TXT_CUR_OFF jr telnet_receive_start .telnet_receive_loop ld a, (telnet_inIAC) ; Is this octet a command/option? or a ld a, (iy + 0) jr nz, telnet_handleIACed cp TELNET_IAC ; Is the next octet a command? jr z, telnet_handleIAC .telnet_receive_escaped255 ld b, a ; Have to swallow a NUL if it follows CR ld a, (telnet_prevchar) cp CR ld a, b ld (telnet_prevchar), a jr nz, telnet_receive_prevnotcr or a jr z, telnet_receive_continue .telnet_receive_prevnotcr call cons_printchar .telnet_receive_continue inc iy dec de .telnet_receive_start ld a, d or e jr nz, telnet_receive_loop call TXT_CUR_ON ret .telnet_handleIACed ld b, a ; Is this octet an option? ld a, (telnet_inOPT) or a jr nz, telnet_handleOPTed ld a, b cp TELNET_IAC ; Is this an escaped IAC (255)? jr z, telnet_handleIACedIAC cp TELNET_WILL ; Is this a WILL, WON'T, DO or DON'T (>250)? jr nc, telnet_handleOPT if SHOW_INFO call printcrlfmaybe ; None of the other commands are supported! call printstringimm defb "[command #", 0 call printdec call printstringimm defb "]", CR, LF, 0 endif xor a .telnet_handleIAC ld (telnet_inIAC), a jr telnet_receive_continue .telnet_handleIACedIAC xor a ld (telnet_inIAC), a dec a ; (255) jr telnet_receive_escaped255 .telnet_handleOPTed ld c, a ; C = type, B = option if SHOW_INFO call printcrlfmaybe call printstringimm defb "[command #", 0 call printdec ld a, b call printstringimm defb ", option #", 0 call printdec call printstringimm defb "]", CR, LF, 0 endif ; Handle the supported options ld a, b cp TELNET_OPT_ECHO ld hl, telnet_remecho jr z, telnet_handle_remsupported cp TELNET_OPT_SGA ld hl, telnet_remsga jr z, telnet_handle_remsupported ; As per RFC854 (page 2, point 3b), if we get any DON'Ts and WON'Ts ; for any options we don't support, we should ignore them, ; since we're already in the local WON'T and remote WON'T mode bit 0, c ; Check for DON'T and WON'T jr z, telnet_handleOPTed_ignore ; Reject requests for options we don't support .telnet_handleOPTed_reject ld a, 249 ; Transform DO into WON'T and WILL into DON'T sub c ld c, a .telnet_handleOPTed_respond ; FIXME this has the potential to blow up if a large bunch of ; options arrives in one go, because of buffer overflow ld hl, (telnet_active_bind + UCP_OFFSETOF_EXTRA + TCP_OFFSETOF_SNDNUN) inc hl inc hl inc hl ld (telnet_active_bind + UCP_OFFSETOF_EXTRA + TCP_OFFSETOF_SNDNUN), hl dec hl push de ld de, telnet_active_bind + UCP_OFFSETOF_EXTRA + TCP_OFFSETOF_SNDNUN ; + 2 above add hl, de pop de ld (hl), TELNET_IAC inc hl ld (hl), c inc hl ld (hl), b .telnet_handleOPTed_ignore xor a ld (telnet_inIAC), a .telnet_handleOPT ld (telnet_inOPT), a jr telnet_receive_continue .telnet_handle_remsupported ; Handle those options for which we support the remote end choosing ; whether it wants to WILL or WON'T, although we insist on WON'T for our side ; As per RFC854 (page 2, point 3b), if we get requests to enter a ; mode we're already in, we should ignore them ; Note for SGA we say we WON'T, but this doesn't matter ; since we never send GA anyway! ld a, c cp TELNET_DO jr z, telnet_handleOPTed_reject ; No, local WON'T! cp TELNET_DONT jr z, telnet_handleOPTed_ignore ; Already in local WON'T mode! cp (hl) jr z, telnet_handleOPTed_ignore ; Already in that remote mode! ld (hl), a ; Store new remote mode add a, 2 ; Transform WILL into DO and WON'T into DON'T ld c, a jr telnet_handleOPTed_respond .telnet_inIAC defs 1 .telnet_inOPT defs 1 .telnet_remecho defs 1 .telnet_remsga defs 1 .telnet_prevchar defs 1 .telnet_linemode defs 1 .telnet_port defs 2 .telnet_abort call printcrlfmaybe call printstringimm defb "[Connection aborted]", CR, LF, 0 jp telnet_done .telnet_linehandler if MARK_HACKS cp &a3 push bc ld bc, telnet_active_bind + UCP_OFFSETOF_EXTRA call z, printcrlfmaybe jp z, tcp_toggle_trace pop bc endif cp HESC ; CTRL-[ jr z, telnet_abort cp CR ; Only accept CR as valid EOL jp nz, keyb_continue ; Everything else is noise ; Do nothing if the connection has not been or is no longer established push bc ld bc, telnet_active_bind + UCP_OFFSETOF_EXTRA call tcp_maysend pop bc jp nc, keyb_continue ; We can only accept the line if SNDNUN is zero ld hl, (telnet_active_bind + UCP_OFFSETOF_EXTRA + TCP_OFFSETOF_SNDNUN) ld a, h or l jp nz, keyb_continue call printcrlf if TELNET_BUFSIZ < 257 ; Make sure there's space for the line. ld a, TELNET_BUFSIZ - 2 ; Need to allow 2 octets for CRLF cp c ld a, BEL jp c, printcharsafe endif inc bc inc bc ld (telnet_active_bind + UCP_OFFSETOF_EXTRA + TCP_OFFSETOF_SNDNUN), bc dec bc dec bc ld a, b or c ld hl, telnet_active_bind + UCP_OFFSETOF_EXTRA + TCP_OFFSETOF_SNDBUF jr z, telnet_linehandler_blank ex de, hl ldir ex de, hl .telnet_linehandler_blank ld (hl), CR inc hl ld (hl), LF ld bc, telnet_active_bind + UCP_OFFSETOF_EXTRA jp tcp_send_now .telnet_keyhandler if MARK_HACKS cp &a3 push bc ld bc, telnet_active_bind + UCP_OFFSETOF_EXTRA jp z, tcp_toggle_trace pop bc endif cp HESC ; CTRL-[ jr z, telnet_abort ; Do nothing if the connection has not been or is no longer established ld bc, telnet_active_bind + UCP_OFFSETOF_EXTRA call tcp_maysend ret nc ; Check for and perform any key expansion ld hl, telnet_noexpand ld (telnet_noexpand + 1), a cp CR ; Expand CR to CRLF jr nz, $ + 5 ; (RFC845 isn't too clear, ld hl, telnet_nel ; but RFC1123 (3.3.1) clarifies) cp HCR ; Allow a bare CR to be input jr nz, $ + 5 ; to satisfy RFC1123 (3.3.1) ld hl, telnet_cr ; (bare LF can be input with CTRL-J) cp KCUB1 jr nz, $ + 5 ld hl, telnet_cub1 cp KCUD1 jr nz, $ + 5 ld hl, telnet_cud1 cp KCUF1 jr nz, $ + 5 ld hl, telnet_cuf1 cp KCUU1 jr nz, $ + 5 ld hl, telnet_cuu1 cp KHOME jr nz, $ + 5 ld hl, telnet_home cp KICH1 jr nz, $ + 5 ld hl, telnet_ich1 cp KDCH1 ; This actually behaves like CLR jr nz, $ + 5 ld hl, telnet_dch1 cp KEND jr nz, $ + 5 ld hl, telnet_end cp KPP jr nz, $ + 5 ld hl, telnet_kpp cp KNP jr nz, $ + 5 ld hl, telnet_knp ; Get number of chars to insert in BC and pointer to them in HL ld c, (hl) ld b, 0 inc hl ; Make sure we're not trying to send some top-bit-set char ld a, (hl) bit 7, a ret nz ; Make sure there's space in the buffer push hl ld hl, (telnet_active_bind + UCP_OFFSETOF_EXTRA + TCP_OFFSETOF_SNDNUN) add hl, bc ; HL = new SNDNUN ld de, TELNET_BUFSIZ + 1 sbc hl, de ; C cleared by ADD HL, BC above pop hl ret nc ; Drat! ; Echo the character if not remote echo push bc ld a, (telnet_remecho) cp TELNET_WILL push hl jr z, telnet_noecho push bc ld a, (hl) cp CR ; This isn't perfect for LF, etc., but, call z, printcrlf ; frankly, who cares about local-echoing them? call nz, printcharsafe pop bc ; FIXME might be nice to support DEL but note current terminfo uses it for checker board! .telnet_noecho ; Add the octet(s) to the buffer ld de, (telnet_active_bind + UCP_OFFSETOF_EXTRA + TCP_OFFSETOF_SNDNUN) ld hl, telnet_active_bind + UCP_OFFSETOF_EXTRA + TCP_OFFSETOF_SNDNUN + 2 add hl, de ex de, hl pop hl ldir pop hl ; Yes, HL, not BC ld bc, telnet_active_bind + UCP_OFFSETOF_EXTRA ld de, (telnet_active_bind + UCP_OFFSETOF_EXTRA + TCP_OFFSETOF_SNDNUN) add hl, de ld (telnet_active_bind + UCP_OFFSETOF_EXTRA + TCP_OFFSETOF_SNDNUN), hl jp tcp_send .telnet_noexpand defb 1, "?" .telnet_nel defb 2, CR, LF .telnet_cr defb 2, CR, NUL .telnet_cub1 defb 3, ESC, "[D" .telnet_cud1 defb 3, ESC, "[B" .telnet_cuf1 defb 3, ESC, "[C" .telnet_cuu1 defb 3, ESC, "[A" .telnet_home defb 4, ESC, "[1~" .telnet_ich1 defb 4, ESC, "[2~" .telnet_dch1 defb 4, ESC, "[3~" .telnet_end defb 4, ESC, "[4~" .telnet_kpp defb 4, ESC, "[5~" .telnet_knp defb 4, ESC, "[6~" .telnet_keyhandler_resolv cp HESC ; It's that or nothing ret nz call resolv_abort jp telnet_done .telnet_connclosed ld a, d cp TCP_CLOSE_REMOTE jp z, tcp_close call printcrlfmaybe cp TCP_CLOSE_NORMAL jr z, telnet_connclosednormal cp TCP_CLOSE_RESET jr z, telnet_connreset call printstringimm defb "[Connection refused by remote host]", CR, LF, 0 jr telnet_done .telnet_connreset call printstringimm defb "[Connection reset by remote host]", CR, LF, 0 jr telnet_done .telnet_connclosednormal call printstringimm defb "[Connection closed by remote host]", CR, LF, 0 .telnet_done .telnet_final call cons_final ; Close TCP socket if open ld hl, telnet_active_bind call tcp_abort ; Give keyboard back to command line input jp keyb_client_done