mirror of
https://github.com/bobbimanners/emailler.git
synced 2024-11-08 19:10:52 +00:00
461 lines
12 KiB
ArmAsm
461 lines
12 KiB
ArmAsm
; minimal dns implementation - requires a DNS server that supports recursion
|
|
|
|
MAX_DNS_MESSAGES_SENT=8 ;timeout after sending 8 messages will be about 7 seconds (1+2+3+4+5+6+7+8)/4
|
|
|
|
.include "../inc/common.i"
|
|
.ifndef NB65_API_VERSION_NUMBER
|
|
.define EQU =
|
|
.include "../inc/nb65_constants.i"
|
|
.endif
|
|
|
|
.export dns_set_hostname
|
|
.export dns_resolve
|
|
.export dns_ip
|
|
.export dns_status
|
|
.import ip65_error
|
|
.import cfg_dns
|
|
|
|
.import parse_dotted_quad
|
|
.import dotted_quad_value
|
|
|
|
.import ip65_process
|
|
|
|
.import udp_add_listener
|
|
.import udp_remove_listener
|
|
|
|
.import udp_callback
|
|
.import udp_send
|
|
|
|
.import udp_inp
|
|
.import output_buffer
|
|
.importzp udp_data
|
|
|
|
.import udp_send_dest
|
|
.import udp_send_src_port
|
|
.import udp_send_dest_port
|
|
.import udp_send_len
|
|
.import check_for_abort_key
|
|
.import timer_read
|
|
|
|
.segment "IP65ZP" : zeropage
|
|
|
|
dns_hostname: .res 2
|
|
|
|
.bss
|
|
|
|
; dns packet offsets
|
|
dns_inp = udp_inp + udp_data
|
|
dns_id = 0
|
|
dns_flags=2
|
|
dns_qdcount=4
|
|
dns_ancount=6
|
|
dns_nscount=8
|
|
dns_arcount=10
|
|
dns_qname=12
|
|
|
|
dns_server_port=53
|
|
dns_client_port_low_byte: .res 1
|
|
|
|
dns_ip: .res 4 ;will be contain ip address of hostname after succesful exection of dns_resolve
|
|
|
|
dns_msg_id: .res 2
|
|
|
|
dns_current_label_length: .res 1
|
|
dns_current_label_offset: .res 1
|
|
|
|
dns_message_sent_count: .res 1
|
|
|
|
dns_packed_hostname: .res 128
|
|
|
|
; dns state machine
|
|
dns_initializing = 1 ; initial state
|
|
dns_query_sent = 2 ; sent a query, waiting for a response
|
|
dns_complete = 3 ; got a good response
|
|
dns_failed = 4 ; got either a 'no such name' or 'recursion declined' response
|
|
|
|
dns_state: .res 1 ; flag indicating the current stage in the dns resolution process
|
|
dns_timer: .res 1
|
|
dns_loop_count: .res 1
|
|
dns_break_polling_loop: .res 1
|
|
|
|
dns_status: .res 2 ; for debugging purposes only (behaviour not garuanteed)
|
|
|
|
hostname_copied: .res 1
|
|
|
|
questions_in_response: .res 1
|
|
|
|
hostname_was_dotted_quad: .res 1
|
|
|
|
.code
|
|
|
|
; sets up for resolution of a hostname to an ip address
|
|
; inputs:
|
|
; AX = pointer to null terminated string that contains either a dns hostname
|
|
; (e.g. "host.example.com",0) or an address in "dotted quad" format,
|
|
; (e.g. "192.168.1.0",0)
|
|
; outputs:
|
|
; carry flag is set on error (i.e. hostname too long), clear otherwise
|
|
dns_set_hostname:
|
|
stax dns_hostname
|
|
;copy the hostname into a buffer suitable to copy directly into the qname field
|
|
;we need to split on dots
|
|
|
|
jsr parse_dotted_quad ; if we are passed an IP address instead of a hostname, don't bother looking it up in dns
|
|
bcs @wasnt_dotted_quad
|
|
;if the string was a dotted quad, then copy the parsed 4 bytes in to dns_ip
|
|
lda #1
|
|
sta hostname_was_dotted_quad
|
|
ldx #3 ; set destination address
|
|
: lda dotted_quad_value,x
|
|
sta dns_ip,x
|
|
dex
|
|
bpl :-
|
|
|
|
rts ;done!
|
|
|
|
@wasnt_dotted_quad:
|
|
|
|
|
|
ldy #0 ;input pointer
|
|
ldx #1 ;output pointer (start at 1, to skip first length offset, which will be filled in later)
|
|
|
|
sty hostname_was_dotted_quad
|
|
sty dns_current_label_length
|
|
sty dns_current_label_offset
|
|
sty hostname_copied
|
|
|
|
@next_hostname_byte:
|
|
lda (dns_hostname),y ;get next char in hostname
|
|
cmp #0 ;are we at the end of the string?
|
|
bne :+
|
|
inc hostname_copied
|
|
bne @set_length_of_last_label
|
|
:
|
|
|
|
cmp #'.' ;do we need to split the labels?
|
|
bne @not_a_dot
|
|
@set_length_of_last_label:
|
|
txa
|
|
pha
|
|
lda dns_current_label_length
|
|
ldx dns_current_label_offset
|
|
sta dns_packed_hostname,x
|
|
lda #0
|
|
sta dns_current_label_length
|
|
pla
|
|
tax
|
|
stx dns_current_label_offset
|
|
lda hostname_copied
|
|
beq @update_counters
|
|
jmp @hostname_done
|
|
@not_a_dot:
|
|
sta dns_packed_hostname,x
|
|
inc dns_current_label_length
|
|
|
|
@update_counters:
|
|
iny
|
|
inx
|
|
bmi @hostname_too_long ;don't allow a hostname of more than 128 bytes
|
|
jmp @next_hostname_byte
|
|
|
|
@hostname_done:
|
|
|
|
lda dns_packed_hostname-1,x ;get the last byte we wrote out
|
|
beq :+ ;was it a zero?
|
|
lda #0
|
|
sta dns_packed_hostname,x ;write a trailing zero (i.e. a zero length label)
|
|
inx
|
|
:
|
|
clc ;no error
|
|
|
|
rts
|
|
|
|
@hostname_too_long:
|
|
lda #NB65_ERROR_INPUT_TOO_LARGE
|
|
sta ip65_error
|
|
sec
|
|
rts
|
|
|
|
; resolve a string containing a hostname (or a dotted quad) to an ip address
|
|
; inputs:
|
|
; cfg_dns must point to a DNS server that supports recursion
|
|
; dns_set_hostname must have been called to load the string to be resolved
|
|
; outputs:
|
|
; carry flag is set if there was an error, clear otherwise
|
|
; dns_ip: set to the ip address of the hostname (if no error)
|
|
dns_resolve:
|
|
lda hostname_was_dotted_quad
|
|
beq @hostname_not_dotted_quad
|
|
clc
|
|
rts ;we already set dns_ip when copying the hostname
|
|
@hostname_not_dotted_quad:
|
|
ldax #dns_in
|
|
stax udp_callback
|
|
lda #53
|
|
inc dns_client_port_low_byte ;each call to resolve uses a different client address
|
|
ldx dns_client_port_low_byte ;so we don't get confused by late replies to a previous call
|
|
jsr udp_add_listener
|
|
|
|
bcc :+
|
|
rts
|
|
:
|
|
|
|
lda #dns_initializing
|
|
sta dns_state
|
|
lda #0 ;reset the "message sent" counter
|
|
sta dns_message_sent_count
|
|
|
|
jsr send_dns_query
|
|
|
|
@dns_polling_loop:
|
|
lda dns_message_sent_count
|
|
adc #1
|
|
sta dns_loop_count ;we wait a bit longer between each resend
|
|
@outer_delay_loop:
|
|
lda #0
|
|
sta dns_break_polling_loop
|
|
jsr timer_read
|
|
stx dns_timer ;we only care about the high byte
|
|
|
|
@inner_delay_loop:
|
|
jsr ip65_process
|
|
jsr check_for_abort_key
|
|
bcc @no_abort
|
|
lda #NB65_ERROR_ABORTED_BY_USER
|
|
sta ip65_error
|
|
rts
|
|
@no_abort:
|
|
|
|
lda dns_state
|
|
cmp #dns_complete
|
|
beq @complete
|
|
cmp #dns_failed
|
|
beq @failed
|
|
|
|
lda #0
|
|
cmp dns_break_polling_loop
|
|
bne @break_polling_loop
|
|
jsr timer_read
|
|
cpx dns_timer ;this will tick over after about 1/4 of a second
|
|
beq @inner_delay_loop
|
|
|
|
dec dns_loop_count
|
|
bne @outer_delay_loop
|
|
|
|
@break_polling_loop:
|
|
jsr send_dns_query
|
|
inc dns_message_sent_count
|
|
lda dns_message_sent_count
|
|
cmp #MAX_DNS_MESSAGES_SENT-1
|
|
bpl @too_many_messages_sent
|
|
jmp @dns_polling_loop
|
|
|
|
@complete:
|
|
|
|
lda #53
|
|
ldx dns_client_port_low_byte
|
|
jsr udp_remove_listener
|
|
rts
|
|
|
|
@too_many_messages_sent:
|
|
@failed:
|
|
lda #53
|
|
ldx dns_client_port_low_byte
|
|
jsr udp_remove_listener
|
|
lda #NB65_ERROR_TIMEOUT_ON_RECEIVE
|
|
sta ip65_error
|
|
sec ;signal an error
|
|
rts
|
|
|
|
send_dns_query:
|
|
ldax dns_msg_id
|
|
inx
|
|
adc #0
|
|
stax dns_msg_id
|
|
stax output_buffer+dns_id
|
|
|
|
ldax #$0001 ;QR =0 (query), opcode=0 (query), AA=0, TC=0,RD=1,RA=0,Z=0,RCODE=0
|
|
stax output_buffer+dns_flags
|
|
ldax #$0100 ;we ask 1 question
|
|
stax output_buffer+dns_qdcount
|
|
ldax #$0000
|
|
stax output_buffer+dns_ancount ;we send no answers
|
|
stax output_buffer+dns_nscount ;we send no name servers
|
|
stax output_buffer+dns_arcount ;we send no authorative records
|
|
|
|
ldx #0
|
|
:
|
|
lda dns_packed_hostname,x
|
|
sta output_buffer+dns_qname,x
|
|
inx
|
|
bpl @hostname_still_ok
|
|
lda #NB65_ERROR_INPUT_TOO_LARGE
|
|
sta ip65_error
|
|
jmp @error_on_send ;if we got past 128 bytes, there's a problem
|
|
@hostname_still_ok:
|
|
cmp #0
|
|
bne :- ;keep looping until we have a zero byte.
|
|
|
|
lda #0
|
|
sta output_buffer+dns_qname,x ;high byte of QTYPE=1 (A)
|
|
sta output_buffer+dns_qname+2,x ;high byte of QLASS=1 (IN)
|
|
lda #1
|
|
sta output_buffer+dns_qname+1,x ;low byte of QTYPE=1 (A)
|
|
sta output_buffer+dns_qname+3,x ;low byte of QLASS=1 (IN)
|
|
|
|
txa
|
|
clc
|
|
adc #(dns_qname+4)
|
|
ldx #0
|
|
stax udp_send_len
|
|
|
|
lda #53
|
|
ldx dns_client_port_low_byte
|
|
stax udp_send_src_port
|
|
|
|
ldx #3 ; set destination address
|
|
: lda cfg_dns,x
|
|
sta udp_send_dest,x
|
|
dex
|
|
bpl :-
|
|
|
|
ldax #dns_server_port ; set destination port
|
|
stax udp_send_dest_port
|
|
ldax #output_buffer
|
|
jsr udp_send
|
|
bcs @error_on_send
|
|
lda #dns_query_sent
|
|
sta dns_state
|
|
|
|
rts
|
|
@error_on_send:
|
|
sec
|
|
rts
|
|
|
|
dns_in:
|
|
lda dns_inp+dns_flags+1 ;
|
|
and #$0f ;get the RCODE
|
|
cmp #0
|
|
beq @not_an_error_response
|
|
|
|
sta dns_status ;anything non-zero is a permanent error (invalid domain, server doesn't support recursion etc)
|
|
sta dns_status+1
|
|
lda #dns_failed
|
|
sta dns_state
|
|
rts
|
|
@not_an_error_response:
|
|
lda dns_inp+dns_qdcount+1
|
|
sta questions_in_response
|
|
cmp #1 ;should be exactly 1 Q in the response (i.e. the one we sent)
|
|
beq :+
|
|
jmp @error_in_response
|
|
:
|
|
lda dns_inp+dns_ancount+1
|
|
bne :+
|
|
jmp @error_in_response ;should be at least 1 answer in response
|
|
: ;we need to skip over the question (we will assume it's the question we just asked)
|
|
ldx #dns_qname
|
|
:
|
|
lda dns_inp,x ;get next length byte in question
|
|
beq :+ ; we're done if length==0
|
|
clc
|
|
txa
|
|
|
|
adc dns_inp,x ;add length of next label to ptr
|
|
adc #1 ;+1 for the length byte itself
|
|
tax
|
|
bcs @error_in_response ;if we overflowed x, then message is too big
|
|
bcc :-
|
|
:
|
|
inx ;skip past the nul byte
|
|
lda dns_inp+1,x
|
|
cmp #1 ;QTYPE should 1
|
|
lda dns_inp+3,x
|
|
cmp #1 ;QCLASS should 1
|
|
bne @error_in_response
|
|
|
|
inx ;skip past the QTYPE/QCLASS
|
|
inx
|
|
inx
|
|
inx
|
|
|
|
;x now points to the start of the answers
|
|
|
|
lda dns_inp,x
|
|
bpl @error_in_response ;we are expecting the high bit to be set (we assume the server will send us back the answer to the question we just asked)
|
|
inx ;skip past the compression
|
|
inx
|
|
;we are now pointing at the TYPE field
|
|
lda dns_inp+1,x ;
|
|
|
|
cmp #5 ; is this a CNAME?
|
|
bne @not_a_cname
|
|
|
|
|
|
txa
|
|
clc
|
|
adc #10 ;skip 2 bytes TYPE, 2 bytes CLASS, 4 bytes TTL, 2 bytes RDLENGTH
|
|
tax
|
|
;we're now pointing at the CNAME record
|
|
ldy #0 ;start of CNAME hostname
|
|
:
|
|
lda dns_inp,x
|
|
beq @last_byte_of_cname
|
|
bmi @found_compression_marker
|
|
sta dns_packed_hostname,y
|
|
inx
|
|
iny
|
|
bmi @error_in_response ;if we go past 128 bytes, something is wrong
|
|
bpl :- ;go get next byte
|
|
@last_byte_of_cname:
|
|
sta dns_packed_hostname,y
|
|
|
|
lda #$ff ;set a status marker so we know whats going on
|
|
sta dns_status
|
|
stx dns_status+1
|
|
|
|
lda #1
|
|
sta dns_break_polling_loop
|
|
|
|
rts ; finished processing - the main dns polling loop should now resend a query, this time for the hostname from the CNAME record
|
|
|
|
@found_compression_marker:
|
|
lda dns_inp+1,x
|
|
tax
|
|
jmp :-
|
|
|
|
@not_a_cname:
|
|
cmp #1 ; should be 1 (A record)
|
|
bne @error_in_response
|
|
txa
|
|
clc
|
|
adc #10 ;skip 2 bytes TYPE, 2 bytes CLASS, 4 bytes TTL, 2 bytes RDLENGTH
|
|
tax
|
|
;we're now pointing at the answer!
|
|
lda dns_inp,x
|
|
sta dns_ip
|
|
|
|
lda dns_inp+1,x
|
|
sta dns_ip+1
|
|
|
|
lda dns_inp+2,x
|
|
sta dns_ip+2
|
|
|
|
lda dns_inp+3,x
|
|
sta dns_ip+3
|
|
|
|
|
|
lda #dns_complete
|
|
sta dns_state
|
|
|
|
lda #1
|
|
sta dns_break_polling_loop
|
|
|
|
@error_in_response:
|
|
|
|
sta dns_status
|
|
stx dns_status+1
|
|
rts
|
|
|
|
|