Repository: CTFd/CTFd

Issue:
Add support for GeoIP to determine country
Public CTFs attract users from across the world. It's really cool to see where people are playing from, so we have the ability for teams to declare their country. But with all of the countries in the world, scrolling through a list when you just want to register and play is not idea. This leads to most teams not having a country set.

To improve the registration flow, we can use a public GeoIP database to determine the country code and automatically select the country. I have the necessary patch to enable this, if this is a feature we're interested in having. The only additional dependency would be the database itself, but we can just ask users to install this themselves and provide the file path to it in the CTFd settings.

![image](https://user-images.githubusercontent.com/3030665/65164666-58c03e00-da0b-11e9-8776-0b68ea9fe1cf.png)



Discussion:
I like this idea and had it myself for mapping IPs to countries in the admin panel. My only concern is the license associated with the MaxMind data. We might be able to skirt those kinds of issues with a library though. 

https://github.com/mitsuhiko/python-geoip
python-geoip is what I used. It still needs access to a DB file though. Check out my patch: https://gist.github.com/grant-h/f9c71a0b3714771111271c72abc66f11

We could avoid licensing issues by requesting that the GeoIP file is downloaded by the end-user (non-free, optional extra).
Their license ([source](https://dev.maxmind.com/geoip/geoip2/geolite2/)) for the GeoLite database is: "The GeoLite2 databases are distributed under the Creative Commons Attribution-ShareAlike 4.0 International License. The attribution requirement may be met by including the following in all advertising and documentation mentioning features of or use of this database." But they have a specific commercial redistribution clause right after: "We also offer redistribution licenses for GeoLite2 databases that allow you to package the databases with your commercial products. For more information, please visit the [GeoLite2 Commercial Redistribution page](https://www.maxmind.com/en/geolite2-commercial-redistribution)." Not sure if this will work for ctfd.io as a company.
The licensing terms aren't super clear and to me in some situations appears conflicting. 

I'd be willing to use a database made from alternative sources:
https://github.com/RobThree/IP2Country/wiki/IP-to-country-databases

Optimistically I'd like to see a database created from alternative sources that could be read with the Maxmind libraries. https://maxmind.github.io/MaxMind-DB/
This one looks great: https://lite.ip2location.com/database/ip-country (download index: https://download.ip2location.com/lite/).
`IP2Location LITE Edition is free package with accuracy up to Class C (192.168.1.X) only.`
Sounds good enough.

```
3. Licensees may copy, distribute, display and perform the work and make derivative works based on IP2Location LITE only if they give the IP2LOCATION.COM credits in the manner specified below.

All sites, advertising materials and documentation mentioning features or use of this database must display the following acknowledgment:
"This site or product includes IP2Location LITE data available from http://www.ip2location.com."
```

We could add this to an open-source disclosures link within CTFd.

Oh and they have a python library ([a single file DB](https://github.com/chrislim2888/IP2Location-Python/blob/master/IP2Location.py)) that reads their compact BIN format! https://github.com/chrislim2888/IP2Location-Python (https://pypi.org/project/IP2Location/). It's Python 3 ready and lookups are `O(log n)` like maxmind.
Here are some sample DBs: https://www.ip2location.com/development-libraries/ip2location/python
Hi ColdHeat, thanks for considering to add us in milestones 3.0. Meanwhile, if you have any questions or require any assistances, please email us at support@ip2location.com. We are looking forward to this integration.

Resolving patch:
diff --git a/CTFd/utils/countries/geoip.py b/CTFd/utils/countries/geoip.py
new file mode 100644
--- /dev/null
+++ b/CTFd/utils/countries/geoip.py
@@ -0,0 +1,15 @@
+import geoacumen
+import maxminddb
+from flask import current_app
+
+IP_ADDR_LOOKUP = maxminddb.open_database(
+    current_app.config.get("GEOIP_DATABASE_PATH", geoacumen.db_path)
+)
+
+
+def lookup_ip_address(addr):
+    response = IP_ADDR_LOOKUP.get(addr)
+    try:
+        return response["country"]["iso_code"]
+    except KeyError:
+        return None
diff --git a/CTFd/utils/initialization/__init__.py b/CTFd/utils/initialization/__init__.py
--- a/CTFd/utils/initialization/__init__.py
+++ b/CTFd/utils/initialization/__init__.py
@@ -20,7 +20,6 @@
     is_setup,
 )
 from CTFd.utils.config.pages import get_pages
-from CTFd.utils.countries import get_countries, lookup_country_code
 from CTFd.utils.dates import isoformat, unix_time, unix_time_millis
 from CTFd.utils.events import EventManager, RedisEventManager
 from CTFd.utils.humanize.words import pluralize
@@ -65,6 +64,8 @@ def init_template_globals(app):
         registration_visible,
         scores_visible,
     )
+    from CTFd.utils.countries import get_countries, lookup_country_code
+    from CTFd.utils.countries.geoip import lookup_ip_address
 
     app.jinja_env.globals.update(config=config)
     app.jinja_env.globals.update(get_pages=get_pages)
@@ -85,6 +86,7 @@ def init_template_globals(app):
     app.jinja_env.globals.update(generate_account_url=generate_account_url)
     app.jinja_env.globals.update(get_countries=get_countries)
     app.jinja_env.globals.update(lookup_country_code=lookup_country_code)
+    app.jinja_env.globals.update(lookup_ip_address=lookup_ip_address)
     app.jinja_env.globals.update(accounts_visible=accounts_visible)
     app.jinja_env.globals.update(challenges_visible=challenges_visible)
     app.jinja_env.globals.update(registration_visible=registration_visible)
diff --git a/populate.py b/populate.py
--- a/populate.py
+++ b/populate.py
@@ -17,6 +17,7 @@
     ChallengeFiles,
     Fails,
     Solves,
+    Tracking,
 )
 from faker import Faker
 
@@ -108,6 +109,10 @@ def gen_file():
     return fake.file_name()
 
 
+def gen_ip():
+    return fake.ipv4()
+
+
 def random_date(start, end):
     return start + datetime.timedelta(
         seconds=random.randint(0, int((end - start).total_seconds()))
@@ -198,6 +203,11 @@ def random_chance():
                     if mode == "teams":
                         user.team_id = random.randint(1, TEAM_AMOUNT)
                     db.session.add(user)
+                    db.session.flush()
+
+                    track = Tracking(ip=gen_ip(), user_id=user.id)
+                    db.session.add(track)
+                    db.session.flush()
                     count += 1
                 except Exception:
                     pass


Test patch: