HeavyThing - maxmind.inc

Jeff Marrison

Table of functions

	; ------------------------------------------------------------------------
	; HeavyThing x86_64 assembly language library and showcase programs
	; Copyright © 2015-2018 2 Ton Digital 
	; Homepage: https://2ton.com.au/
	; Author: Jeff Marrison <jeff@2ton.com.au>
	;       
	; This file is part of the HeavyThing library.
	;       
	; HeavyThing is free software: you can redistribute it and/or modify
	; it under the terms of the GNU General Public License, or
	; (at your option) any later version.
	;       
	; HeavyThing is distributed in the hope that it will be useful, 
	; but WITHOUT ANY WARRANTY; without even the implied warranty of
	; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
	; GNU General Public License for more details.
	;       
	; You should have received a copy of the GNU General Public License along
	; with the HeavyThing library. If not, see <http://www.gnu.org/licenses/>.
	; ------------------------------------------------------------------------
	; maxmind.inc: http://dev.maxmind.com/geoip/geoip2/geolite2/ support
	;
	; The routines herein expect CSV formatted GeoLite2 for its data, and this
	; only parses for a City, Country IPv4 lookup (we don't bother with the
	; rest of the GeoLite2 data).
	; 

if used maxmind$new | defined include_everything

maxmind_blockmap_ofs = 0		; unsignedmap keyed by IPv4, value is endIP and locID
maxmind_locmap_ofs = 8			; unsignedmap keyed by locID, value is string City, Country
maxmind_cities_ofs = 16			; stringmap, keyed by City, Country ... no value
maxmind_size = 24



	; two arguments: rdi == string location of GeoLite2-City-Blocks-IPv4.csv filename,
	;    rsi == string location of the GeoLite2-City-Locations csv filename
	; returns a new maxmind object, or null if files invalid/not found
	; (NOTE: sane maxmind files are required, no bounds checking is performed)
falign
maxmind$new:
	prolog	maxmind$new
	push	rbx r12 r13 r14 r15
	mov	r12, rdi
	mov	r13, rsi
	xor	esi, esi
	call	privmapped$new
	test	rax, rax
	jz	.failexit
	mov	r12, rax
	mov	r14, [rax+privmapped_base_ofs]
	mov	r15, [rax+privmapped_size_ofs]
	mov	rdi, r13
	xor	esi, esi
	test	r15, r15
	jz	.cityfail
	call	privmapped$new
	test	rax, rax
	jz	.cityfail
	mov	r13, rax
	cmp	qword [rax+privmapped_size_ofs], 0
	je	.bothfail
	; both privmapped objects are valid
	mov	edi, maxmind_size
	call	heap$alloc
	mov	rbx, rax
	xor	edi, edi
	call	unsignedmap$new
	mov	[rbx+maxmind_blockmap_ofs], rax
	xor	edi, edi
	call	unsignedmap$new
	mov	[rbx+maxmind_locmap_ofs], rax
	xor	edi, edi
	call	stringmap$new
	mov	[rbx+maxmind_cities_ofs], rax

	push	rbp r12
	sub	rsp, 8
.blocksouter_reset:
	xor	ebp, ebp
	mov	r12, rsp
calign
.blocksouter:
	movzx	eax, byte [r14]
	cmp	eax, '.'
	je	.blocksouter_separator
	cmp	eax, '/'
	je	.blocksouter_addrdone
	cmp	eax, '0'
	jb	.skipblockline
	cmp	eax, '9'
	ja	.skipblockline
	; otherwise, a numeric digit
	sub	eax, '0'
	; mul whatever is in ebp by 10 then add this digit
	imul	ebp, ebp, 10
	add	ebp, eax
	add	r14, 1
	sub	r15, 1
	jnz	.blocksouter
.blocksdone:
	; locations next up
	mov	r14, [r13+privmapped_base_ofs]
	mov	r15, [r13+privmapped_size_ofs]
.cityouter_reset:
	xor	ebp, ebp
	mov	r12d, 5
calign
.cityouter:
	movzx	eax, byte [r14]
	cmp	eax, ','
	je	.cityouter_locIDdone
	cmp	eax, '0'
	jb	.skipcityline
	cmp	eax, '9'
	ja	.skipcityline
	; otherwise, a numeric digit
	sub	eax, '0'
	imul	ebp, ebp, 10
	add	ebp, eax
	add	r14, 1
	sub	r15, 1
	jnz	.cityouter
.citydone:
	; cleanup our stack
	add	rsp, 8
	pop	r12 rbp
	; free our privmapped goods
	mov	rdi, r13
	call	privmapped$destroy
	mov	rdi, r12
	call	privmapped$destroy
	; return is rbx
	mov	rax, rbx
.failexit:
	pop	r15 r14 r13 r12 rbx
	epilog
calign
.cityouter_locIDdone:
	movzx	eax, byte [r14]
	cmp	eax, ','
	je	.cityouter_countryscan
	add	r14, 1
	sub	r15, 1
	jnz	.cityouter_locIDdone
	jz	.citydone
calign
.cityouter_countryscan:
	add	r14, 1
	sub	r15, 1
	jz	.citydone
	sub	r12d, 1
	jnz	.cityouter_locIDdone
	; r14 now sitting on the country_name
	mov	[rsp], r14
	; walk forward again til next comma
calign
.cityouter_countryendscan:
	movzx	eax, byte [r14]
	cmp	eax, ','
	je	.cityouter_countryend
	add	r14, 1
	sub	r15, 1
	jnz	.cityouter_countryendscan
	jz	.citydone
calign
.cityouter_countryend:
	; r14 now sitting on the comma after the country
	mov	rdi, [rsp]
	mov	rsi, r14
	sub	rsi, rdi
	call	string$from_utf8
	; stick that somewhere temporarily
	mov	[r13+privmapped_user_ofs], rax
	; so the user var of our privmapped object contains the country name
	; we are still sitting on the comma after the country... so we
	; need to skip 5 more commas
	mov	r12d, 5
calign
.cityouter_citysearch:
	movzx	eax, byte [r14]
	cmp	eax, ','
	je	.cityouter_cityscan
	add	r14, 1
	sub	r15, 1
	jnz	.cityouter_citysearch
	jz	.citydone
calign
.cityouter_cityscan:
	add	r14, 1
	sub	r15, 1
	jz	.citydone
	sub	r12d, 1
	jnz	.cityouter_citysearch
	; r14 now sitting on the city_name
	mov	[rsp], r14
	; walk forward again til next comma
calign
.cityouter_cityendscan:
	movzx	eax, byte [r14]
	cmp	eax, ','
	je	.cityouter_cityend
	add	r14, 1
	sub	r15, 1
	jnz	.cityouter_cityendscan
	jz	.citydone
calign
.cityouter_cityend:
	; r14 now sitting on the comma after the city
	mov	rdi, [rsp]
	mov	rsi, r14
	sub	rsi, rdi
	call	string$from_utf8
	mov	r12, rax		; save that
	cmp	qword [r12], 0
	je	.cityouter_countryonly
	mov	rdi, rax
	mov	rsi, .commaspace
	call	string$concat
	mov	rdi, r12
	mov	r12, rax
	call	heap$free
.cityouter_countryonly:
	mov	rdi, r12
	mov	rsi, [r13+privmapped_user_ofs]
	call	string$concat
	mov	rdi, r12
	mov	r12, rax
	call	heap$free
	mov	rdi, [r13+privmapped_user_ofs]
	call	heap$free
	; so now, r12 contains City, Country
	; and ebp still contains our locID key
	; get rid of double quotes if we have any
	mov	rdi, r12
	mov	esi, '"'
	call	string$indexof_charcode
	cmp	rax, 0
	jl	.noquotes
	mov	rdi, r12
	mov	rsi, .doublequote
	mov	rdx, .emptystring
	call	string$replace
	mov	rdi, r12
	mov	r12, rax
	call	heap$free
.noquotes:
	; first up, see if we already know about this city, country in our cities map
	mov	rdi, [rbx+maxmind_cities_ofs]
	mov	rsi, r12
	call	stringmap$find
	test	rax, rax
	jz	.cityouter_newcity
	; otherwise, we can reuse the _avlofs_key from this node
	mov	rdi, r12
	mov	r12, [rax+_avlofs_key]
	call	heap$free
.cityouter_insert:
	; insert into our locmap
	mov	rdi, [rbx+maxmind_locmap_ofs]
	mov	esi, ebp	; locID
	mov	rdx, r12	; City, Country string
	call	unsignedmap$insert_unique
	; fallthrough to skipcityline
calign
.skipcityline:
	movzx	eax, byte [r14]
	add	r14, 1
	sub	r15, 1
	jz	.citydone
	cmp	eax, 10
	jne	.skipcityline
	jmp	.cityouter_reset
cleartext .commaspace, ', '
cleartext .doublequote, '"'
cleartext .emptystring, ''
calign
.cityouter_newcity:
	; insert our r12 string as the key into our cities map
	mov	rdi, [rbx+maxmind_cities_ofs]
	mov	rsi, r12
	xor	edx, edx	; we don't care about the value (it is a set)
	call	stringmap$insert_unique
	; go ahead and insert
	jmp	.cityouter_insert
calign
.skipblockline:
	movzx	eax, byte [r14]
	add	r14, 1
	sub	r15, 1
	jz	.blocksdone
	cmp	eax, 10
	jne	.skipblockline
	jmp	.blocksouter_reset
calign
.blocksouter_addrdone:
	; put the final byte into r12
	mov	eax, ebp
	mov	byte [r12], al
	sub	r12, 3
	mov	r12d, [r12]
	xor	ebp, ebp
	; r12d now contains our IPv4 key, netblock size is next (we are sitting on the /)
	add	r14, 1
	sub	r15, 1
	jz	.blocksdone
calign
.blocksouter_netblock:
	movzx	eax, byte [r14]
	cmp	eax, ','
	je	.blocksouter_netblockdone
	cmp	eax, '0'
	jb	.skipblockline
	cmp	eax, '9'
	ja	.skipblockline
	sub	eax, '0'
	imul	ebp, ebp, 10
	add	ebp, eax
	add	r14, 1
	sub	r15, 1
	jnz	.blocksouter_netblock
	jz	.blocksdone
calign
.blocksouter_netblockdone:
	; we are sitting on a , which means ebp contains our netblock size
	mov	ecx, 32
	mov	eax, 1
	sub	ecx, ebp
	shl	rax, cl
	sub	rax, 1
	; bswap the network address so we can numerically add this amount to it
	mov	edx, r12d
	bswap	edx
	; eax now contains our block size that we'll also keep track of
	lea	ebp, [edx+eax]		; end address
	; we still need to parse out the locID, so we can save ebp into our stack
	mov	[rsp], rbp		; [rsp] == end address
	xor	ebp, ebp
	add	r14, 1
	sub	r15, 1
	jz	.blocksdone
calign
.blocksouter_locID:
	movzx	eax, byte [r14]
	cmp	eax, ','
	je	.blocksouter_locIDdone
	cmp	eax, '0'
	jb	.skipblockline
	cmp	eax, '9'
	ja	.skipblockline
	sub	eax, '0'
	imul	ebp, ebp, 10
	add	ebp, eax
	add	r14, 1
	sub	r15, 1
	jnz	.blocksouter_locID
	jz	.blocksdone
calign
.blocksouter_locIDdone:
	; we are sitting on a , which means ebp contains our locID
	bswap	r12d
	shl	rbp, 32			; upper 32 bits == locID
	or	rbp, [rsp]		; lower 32 bits == end address
	mov	rdi, [rbx+maxmind_blockmap_ofs]
	mov	esi, r12d
	mov	rdx, rbp
	call	unsignedmap$insert_unique
	; we aren't interested in the remainder of the line:
	jmp	.skipblockline
calign
.blocksouter_separator:
	mov	eax, ebp
	xor	ebp, ebp
	mov	byte [r12], al
	add	r12, 1
	add	r14, 1
	sub	r15, 1
	jz	.blocksdone
	jmp	.blocksouter
.bothfail:
	mov	rdi, r13
	call	privmapped$destroy
.cityfail:
	mov	rdi, r12
	call	privmapped$destroy
	xor	eax, eax
	pop	r15 r14 r13 r12 rbx
	epilog

end if



if used maxmind$destroy | defined include_everything
	; single argument in rdi: maxmind object to destroy
falign
maxmind$destroy:
	prolog	maxmind$destroy
	push	rbx
	mov	rbx, rdi
	mov	rdi, [rdi+maxmind_cities_ofs]
	mov	rsi, heap$free
	call	stringmap$clear
	mov	rdi, [rbx+maxmind_cities_ofs]
	call	heap$free
	mov	rdi, [rbx+maxmind_locmap_ofs]
	xor	esi, esi
	call	unsignedmap$clear
	mov	rdi, [rbx+maxmind_locmap_ofs]
	call	heap$free
	mov	rdi, [rbx+maxmind_blockmap_ofs]
	xor	esi, esi
	call	unsignedmap$clear
	mov	rdi, [rbx+maxmind_blockmap_ofs]
	call	heap$free
	mov	rdi, rbx
	call	heap$free
	pop	rbx
	epilog

end if

if used maxmind$citycountry_ipv4 | defined include_everything
	; two arguments: rdi == maxmind object, esi == numeric ipv4 address
	; returns a string of City, Country ... or Country by itself if no City, or empty string if not found
falign
maxmind$citycountry_ipv4:
	prolog	maxmind$citycountry_ipv4
	bswap	esi				; first octet of IPv4 == highest
	push	rbx r12
	mov	rbx, rdi
	mov	r12d, esi
	; first up: lowerbound returns us the first >= our address:
	mov	rdi, [rdi+maxmind_blockmap_ofs]
	call	unsignedmap$lowerbound
	test	rax, rax
	jz	.retempty
	; if the node's key > our address, go back one block
	cmp	r12d, [rax+_avlofs_key]
	jb	.backone			; if our address is < this node, go back one
.nodeokay:
	; see if our address is inside the lower 32 bits of the blockmap
	cmp	r12d, [rax+_avlofs_value]	; lower 32 bits == end address
	ja	.retempty
	; otherwise, upper 32 bits is our locID:
	mov	rdi, [rbx+maxmind_locmap_ofs]
	mov	esi, [rax+_avlofs_value+4]	; upper 32 bits == locID
	call	unsignedmap$find_value
	test	eax, eax
	jz	.retempty
	mov	rax, rdx
	pop	r12 rbx
	epilog
.backone:
	mov	rax, [rax+_avlofs_prev]
	test	rax, rax
	jnz	.nodeokay
.retempty:
	mov	rax, .emptystring
	pop	r12 rbx
	epilog
cleartext .emptystring, ''

end if

if used maxmind$citycountry | defined include_everything
	; two arguments: rdi == maxmind object, rsi == string ipv4 address
	; returns a string of City, Country ... or Country by itself if no City, or empty string if not found
falign
maxmind$citycountry:
	prolog	maxmind$citycountry
	mov	rdx, [rsi]
	test	rdx, rdx
	jz	.retempty
	add	rsi, 8
	xor	ecx, ecx
calign
.dig1:
if string_bits = 32
	mov	eax, [rsi]
	add	rsi, 4
else
	movzx	eax, word [rsi]
	add	rsi, 2
end if
	cmp	eax, '.'
	je	.dig1done
	cmp	eax, '0'
	jb	.retempty
	cmp	eax, '9'
	ja	.retempty
	sub	eax, '0'
	imul	ecx, ecx, 10
	add	ecx, eax
	sub	edx, 1
	jnz	.dig1
.retempty:
	mov	rax, .emptystring
	epilog
cleartext .emptystring, ''
calign
.dig1done:
	sub	edx, 1
	jz	.retempty
	; first octet is in ecx
	mov	r8d, ecx
	xor	ecx, ecx
calign
.dig2:
if string_bits = 32
	mov	eax, [rsi]
	add	rsi, 4
else
	movzx	eax, word [rsi]
	add	rsi, 2
end if
	cmp	eax, '.'
	je	.dig2done
	cmp	eax, '0'
	jb	.retempty
	cmp	eax, '9'
	ja	.retempty
	sub	eax, '0'
	imul	ecx, ecx, 10
	add	ecx, eax
	sub	edx, 1
	jnz	.dig2
	jmp	.retempty
calign
.dig2done:
	sub	edx, 1
	jz	.retempty
	; second octet is in ecx
	shl	ecx, 8
	or	r8d, ecx
	xor	ecx, ecx
calign
.dig3:
if string_bits = 32
	mov	eax, [rsi]
	add	rsi, 4
else
	movzx	eax, word [rsi]
	add	rsi, 2
end if
	cmp	eax, '.'
	je	.dig3done
	cmp	eax, '0'
	jb	.retempty
	cmp	eax, '9'
	ja	.retempty
	sub	eax, '0'
	imul	ecx, ecx, 10
	add	ecx, eax
	sub	edx, 1
	jnz	.dig3
	jmp	.retempty
calign
.dig3done:
	sub	edx, 1
	jz	.retempty
	; third octet is in ecx
	shl	ecx, 16
	or	r8d, ecx
	xor	ecx, ecx
calign
.dig4:
if string_bits = 32
	mov	eax, [rsi]
	add	rsi, 4
else
	movzx	eax, word [rsi]
	add	rsi, 2
end if
	cmp	eax, '0'
	jb	.retempty
	cmp	eax, '9'
	ja	.retempty
	sub	eax, '0'
	imul	ecx, ecx, 10
	add	ecx, eax
	sub	edx, 1
	jnz	.dig4
	; fourth octet is in ecx
	shl	ecx, 24
	or	r8d, ecx
	mov	esi, r8d
	call	maxmind$citycountry_ipv4
	epilog

end if