diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..3702e78 --- /dev/null +++ b/.env.sample @@ -0,0 +1,20 @@ +X_DEBUG=True +X_DOMAIN="https://www.geoip.ir" +X_MYSQL_USERNAME='root' +X_MYSQL_PASSWORD='' +X_MYSQL_HOST='localhost' +X_MYSQL_PORT='3306' +X_MYSQL_DATABASE_NAME='geoip' + + + +X_CAPTCHA_PUBLIC='x' +X_CAPTCHA_PRIVATE='x' +X_CAPTCHA_ENABLE=False + +X_CACHE_TYPE='NullCache' +X_REDIS_HOST='localhost' +X_REDIS_PORT='6379' +X_REDIS_PASSWORD='' +X_REDIS_DB='0' +X_REDIS_URL='redis://:@localhost:6379/0' \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5452d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,175 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so +.idea +.env +ADD-DATA-PY +insert_data.py +deploy_to_server.py +change_github_to_deploy.py +MakeServer + + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +GeoIpConfig/data.CSV +GeoIpConfig/data.json +GeoIpConfig/*.zip +GeoIpConfig/*.json +GeoIpConfig/*.csv +GeoIpConfig/*.rar diff --git a/GeoIpApi/__init__.py b/GeoIpApi/__init__.py new file mode 100644 index 0000000..b3da701 --- /dev/null +++ b/GeoIpApi/__init__.py @@ -0,0 +1,7 @@ +from flask import Blueprint + + +api = Blueprint("api", __name__) + +import GeoIpApi.views +import GeoIpApi.model \ No newline at end of file diff --git a/GeoIpApi/model.py b/GeoIpApi/model.py new file mode 100644 index 0000000..d350888 --- /dev/null +++ b/GeoIpApi/model.py @@ -0,0 +1,44 @@ +from GeoIpCore.model import BaseTable +from sqlalchemy import Column, String, BIGINT, JSON, DECIMAL + + +class IPV4(BaseTable): + """This Class Contain Ipv4 range and name""" + __tablename__ = "IPV4" + StartRange = Column(BIGINT, unique=False, nullable=False) + EndRange = Column(BIGINT, nullable=False, unique=True) + + CountryCode = Column(String(64), unique=False, nullable=False) + CountryName = Column(String(64), unique=False, nullable=False) + StateName = Column(String(64), unique=False, nullable=False) + CityName = Column(String(64), unique=False, nullable=False) + Lat = Column(String(64), unique=False, nullable=False) + Long = Column(String(64), unique=False, nullable=False) + + ZipCode = Column(String(64), nullable=False, unique=False) + TimeZone = Column(String(64), unique=False, nullable=False) + + +class CountryInfo(BaseTable): + __tablename__ = "CountryInfo" + CommonName = Column(String(255), unique=False, nullable=False) + OfficialName = Column(String(255), unique=False, nullable=False) + CountryCode = Column(String(64), unique=False, nullable=False) + Info = Column(JSON, unique=False, nullable=False) + + +class IPV6(BaseTable): + """This Class Contain Ipv6 range and name""" + __tablename__ = "IPV6" + StartRange = Column(DECIMAL(scale=0, precision=36), unique=False, nullable=False) + EndRange = Column(DECIMAL(scale=0, precision=36), nullable=False, unique=True) + + CountryCode = Column(String(64), unique=False, nullable=False) + CountryName = Column(String(64), unique=False, nullable=False) + StateName = Column(String(64), unique=False, nullable=False) + CityName = Column(String(64), unique=False, nullable=False) + Lat = Column(String(64), unique=False, nullable=False) + Long = Column(String(64), unique=False, nullable=False) + + ZipCode = Column(String(64), nullable=False, unique=False) + TimeZone = Column(String(64), unique=False, nullable=False) diff --git a/GeoIpApi/utils.py b/GeoIpApi/utils.py new file mode 100644 index 0000000..101f5c6 --- /dev/null +++ b/GeoIpApi/utils.py @@ -0,0 +1,312 @@ +import json +import time +import ipaddress + +from flask import jsonify +from GeoIpApi.model import CountryInfo, IPV4 as IPV4db, IPV6 as IPV6db +from GeoIpConfig import BASE_DOMAIN + +def Convert2_IPv4(ip:str) -> ipaddress.IPv4Address: + """ + this function take an ip and convert it to ipaddress.ipv4 + """ + try: + ip = ipaddress.ip_address(ip) + except ValueError: + return False + + if not isinstance(ip, ipaddress.IPv4Address): + return False + + return ip + +def Convert2_IPv6(ip:str) -> ipaddress.IPv6Address: + """ + this function take an ip and convert it to ipaddress.ipv4 + """ + try: + ip = ipaddress.ip_address(ip) + except ValueError: + return False + + if not isinstance(ip, ipaddress.IPv6Address): + return False + + return ip + + + +def JsonAnswer(data:dict={},http_status=200 ): + """Return a Json Answer""" + return jsonify(data), http_status + +class IPv6erializer: + """ + This CLass Serialize IPV4 Data for sending back to user + """ + __IPV6 = None + __DATA = {} + + def __init__(self, IpV6Object: IPV6db): + if not isinstance(IpV6Object, IPV6db): + raise ValueError("input most be a ip table") + + self.__IPV6 = IpV6Object + + + def serializer(self): + """ + this method add base info about ip + """ + data = { + "Country": { + "name":self.__IPV6.CountryName, + }, + "CountryCode": self.__IPV6.CountryCode, + "City": self.__IPV6.CityName, + "State": self.__IPV6.StateName, + "Latitude": self.__IPV6.Lat, + "Longitude": self.__IPV6.Long, + "ZipCode": self.__IPV6.ZipCode, + "TimeZone": self.__IPV6.TimeZone, + "IPv6": { + "ip": self.__IPV6.octet_ip or "Null", + "decimal": self.__IPV6.decimal_ip or "Null", + "hex": self.__IPV6.hex_ip or "Null", + }, + "x-Response-BY":f"{BASE_DOMAIN}/api/v1/ipv6/{self.__IPV6.octet_ip}" + } + self.__DATA.update(data) + + + def Additionalserializer(self, CountryINFO): + """ + this method add additional info about ip + /true + """ + if CountryINFO: + data = json.loads(CountryINFO.Info) + country = { + "officialName":data["name"]["official"], + "nativeName":data["name"]["nativeName"], + "capital":data["capital"], + "capitalInfo":data["capitalInfo"], + "region":data["region"], + "subregion":data["subregion"], + "languages":data["languages"], + "translations":data["translations"], + "flag":data["flag"], + "maps":data["maps"], + "continents":data["continents"], + "flags":data["flags"] + } + dt = { + "tld":data["tld"], + } + + self.__DATA["Country"].update(country) + self.__DATA.update(dt) + + def getSerialize(self): + return self.__DATA + + + + +class IPv4Serializer: + """ + This CLass Serialize IPV4 Data for sending back to user + """ + __IPV4 = None + __DATA = {} + + def __init__(self, IpV4Object: IPV4db): + if not isinstance(IpV4Object, IPV4db): + raise ValueError("input most be a ip table") + + self.__IPV4 = IpV4Object + + + def serializer(self): + """ + this method add base info about ip + """ + data = { + "Country": { + "name":self.__IPV4.CountryName, + }, + "CountryCode": self.__IPV4.CountryCode, + "City": self.__IPV4.CityName, + "State": self.__IPV4.StateName, + "Latitude": self.__IPV4.Lat, + "Longitude": self.__IPV4.Long, + "ZipCode": self.__IPV4.ZipCode, + "TimeZone": self.__IPV4.TimeZone, + "IPv4": { + "octet": self.__IPV4.octet_ip or "Null", + "decimal": self.__IPV4.decimal_ip or "Null", + "hex": self.__IPV4.hex_ip or "Null", + }, + "x-Response-BY":f"{BASE_DOMAIN}/api/v1/ipv4/{self.__IPV4.octet_ip}" + } + self.__DATA.update(data) + + + def Additionalserializer(self, CountryINFO): + """ + this method add additional info about ip + /true + """ + if CountryINFO: + data = json.loads(CountryINFO.Info) + country = { + "officialName":data["name"]["official"], + "nativeName":data["name"]["nativeName"], + "capital":data["capital"], + "capitalInfo":data["capitalInfo"], + "region":data["region"], + "subregion":data["subregion"], + "languages":data["languages"], + "translations":data["translations"], + "flag":data["flag"], + "maps":data["maps"], + "continents":data["continents"], + "flags":data["flags"] + } + dt = { + "tld":data["tld"], + } + + self.__DATA["Country"].update(country) + self.__DATA.update(dt) + + def getSerialize(self): + return self.__DATA + + + + +def search_in_ipv4(octetIP, extera=False): + """ + + """ + IPV4 = Convert2_IPv4(octetIP) + if not IPV4: + return JsonAnswer( + data= + { + "message": "invalid IP address", + "status": "False", + "ip": octetIP, + "time": (time.time()) + }, http_status=400) + + if IPV4.is_private: + return JsonAnswer( + data= + { + "message": "IP address is private", + "status": "False", + "ip": octetIP, + "time": (time.time()) + },http_status=400) + + if IPV4.is_reserved: + return JsonAnswer( + data= + { + "message": "IP address is reserved", + "status": "False", + "ip": octetIP, + "time": (time.time()) + },http_status=400) + + decimalIP = int(IPV4) + ipLOOKup = IPV4db.query.filter(IPV4db.StartRange <= decimalIP).filter(IPV4db.EndRange >= decimalIP).first() + + if not ipLOOKup: + return JsonAnswer( + data= + { + "message": "Cant Find ip in db", + "status": "False", + "ip": octetIP, + "time": str(time.time()) + },http_status=404) + + ipLOOKup.decimal_ip = decimalIP + ipLOOKup.hex_ip = str(hex(decimalIP)) + ipLOOKup.octet_ip = octetIP + + serializer = IPv4Serializer(ipLOOKup) + serializer.serializer() + + if extera: + countryDB = CountryInfo.query.filter(CountryInfo.CountryCode == ipLOOKup.CountryCode).first() + serializer.Additionalserializer(countryDB) + + return jsonify(serializer.getSerialize()), 200 + + + + +def search_in_ipv6(octetIP, extera=False): + """ + Search in IPV6 Tables for ip + """ + IPV6 = Convert2_IPv6(octetIP) + if not IPV6: + return JsonAnswer( + data= + { + "message": "invalid IP address", + "status": "False", + "ip": octetIP, + "time": (time.time()) + }, http_status=400) + + + decimalIP = int(IPV6) + ipLOOKup = IPV6db.query.filter(IPV6db.StartRange <= decimalIP).filter(IPV6db.EndRange >= decimalIP).first() + + if not ipLOOKup: + if IPV6.is_private: + return JsonAnswer( + data= + { + "message": "IP address is private", + "status": "False", + "ip": octetIP, + "time": (time.time()) + }, http_status=400) + if IPV6.is_reserved: + return JsonAnswer( + data= + { + "message": "IP address is reserved", + "status": "False", + "ip": octetIP, + "time": (time.time()) + }, http_status=400) + + return JsonAnswer( + data= + { + "message": "Cant Find ip in db", + "status": "False", + "ip": octetIP, + "time": str(time.time()) + }, http_status=404) + + ipLOOKup.decimal_ip = decimalIP + ipLOOKup.hex_ip = str(hex(decimalIP)) + ipLOOKup.octet_ip = octetIP + + serializer = IPv6erializer(ipLOOKup) + serializer.serializer() + + if extera: + countryDB = CountryInfo.query.filter(CountryInfo.CountryCode == ipLOOKup.CountryCode).first() + serializer.Additionalserializer(countryDB) + + return jsonify(serializer.getSerialize()), 200 \ No newline at end of file diff --git a/GeoIpApi/views.py b/GeoIpApi/views.py new file mode 100644 index 0000000..998add6 --- /dev/null +++ b/GeoIpApi/views.py @@ -0,0 +1,42 @@ +from GeoIpApi import api +from GeoIpCore.extensions import cache +from GeoIpApi.utils import search_in_ipv4, JsonAnswer, search_in_ipv6 +from GeoIpCore.extensions import limiter + + +@api.route("/ipv4//", methods=["GET"]) +@limiter.limit("60 per minute") +@cache.cached(timeout=43200) +def process_ip_v4(octetIP): + print("Answer Queried in db") + return search_in_ipv4(octetIP) + + +@api.route("/ipv4//", methods=["GET"]) +@limiter.limit("60 per minute") +@cache.cached(timeout=43200) +def process_ip_v4_extera(octetIP, extra): + print("Answer Queried in db") + if extra.lower() != "true": + return JsonAnswer({"message": "Invalid Params", "status": "false", "x-Response-By": "https://ip2geo.ip"}, + http_status=400) + return search_in_ipv4(octetIP, True) + + +@api.route("/ipv6//", methods=["GET"]) +@limiter.limit("60 per minute") +@cache.cached(timeout=43200) +def process_ip_v6(octetIP): + print("Answer Queried in db") + return search_in_ipv6(octetIP) + + +@api.route("/ipv6//", methods=["GET"]) +@limiter.limit("60 per minute") +@cache.cached(timeout=43200) +def process_ip_v6_extera(octetIP, extra): + print("Answer Queried in db") + if extra.lower() != "true": + return JsonAnswer({"message": "Invalid Params", "status": "false", "x-Response-By": "https://ip2geo.ip"}, + http_status=400) + return search_in_ipv6(octetIP, True) diff --git a/GeoIpConfig/__init__.py b/GeoIpConfig/__init__.py new file mode 100644 index 0000000..1155b74 --- /dev/null +++ b/GeoIpConfig/__init__.py @@ -0,0 +1,56 @@ +import os +import datetime +import secrets +from pathlib import Path +from GeoIpCore.extensions import ServerRedis +from dotenv import load_dotenv + + +BASE_DIR = Path(__file__).parent.parent +load_dotenv() + + +USERNAME_DB = os.environ.get("X_MYSQL_USERNAME") +PASSWORD_DB = os.environ.get("X_MYSQL_PASSWORD") +HOST_DB = os.environ.get("X_MYSQL_HOST") +PORT_DB = os.environ.get("X_MYSQL_PORT") +NAME_DB = os.environ.get("X_MYSQL_DATABASE_NAME") + +BASE_DOMAIN = os.environ.get("X_DOMAIN", "HTTPS://DOMAIN.IR") + +class config: + SECRET_KEY = os.environ.get("X_SECRET_KEY", secrets.token_hex(128)) + SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{USERNAME_DB}:{PASSWORD_DB}@{HOST_DB}:{PORT_DB}/{NAME_DB}" + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # session configuration + SESSION_TYPE = "redis" + SESSION_PERMANENT = False + PERMANENT_SESSION_LIFETIME = datetime.timedelta(minutes=16) + SESSION_COOKIE_SECURE = True + SESSION_COOKIE_HTTPONLY = True + SESSION_COOKIE_SAMESITE = 'Lax' + SESSION_COOKIE_NAME = '_session_cookie_' + SESSION_USE_SIGNER = True + SESSION_REDIS = ServerRedis + + # recaptcha v2 + RECAPTCHA_PUBLIC_KEY = os.environ.get("X_CAPTCHA_PUBLIC") + RECAPTCHA_PRIVATE_KEY = os.environ.get("X_CAPTCHA_PRIVATE") + RECAPTCHA_ENABLED = os.environ.get("X_CAPTCHA_ENABLE", True) + + # caching config + # CACHE_TYPE = "RedisCache" # NullCache for disable Flask-Caching related configs + CACHE_TYPE = os.environ.get("X_CACHE_TYPE") + CACHE_DEFAULT_TIMEOUT = ((60*60)*12) + CACHE_REDIS_HOST = os.environ.get("X_REDIS_HOST") + CACHE_REDIS_PORT = os.environ.get("X_REDIS_PORT") + CACHE_REDIS_PASSWORD = os.environ.get("X_REDIS_PASSWORD") + CACHE_REDIS_DB = os.environ.get("X_REDIS_DB") + CACHE_REDIS_URL = os.environ.get("X_REDIS_URL") + + + DEBUG = True if os.environ.get("X_DEBUG") == 'True' else False + FLASK_DEBUG = True if os.environ.get("X_DEBUG") == 'True' else False + + diff --git a/GeoIpConfig/constans/http_status_code.py b/GeoIpConfig/constans/http_status_code.py new file mode 100644 index 0000000..4b7f95a --- /dev/null +++ b/GeoIpConfig/constans/http_status_code.py @@ -0,0 +1,58 @@ + +HTTP_200_OK = 200 +HTTP_201_CREATED = 201 +HTTP_202_ACCEPTED = 202 +HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203 +HTTP_204_NO_CONTENT = 204 +HTTP_205_RESET_CONTENT = 205 +HTTP_206_PARTIAL_CONTENT = 206 +HTTP_207_MULTI_STATUS = 207 +HTTP_208_ALREADY_REPORTED = 208 +HTTP_226_IM_USED = 226 +HTTP_300_MULTIPLE_CHOICES = 300 +HTTP_301_MOVED_PERMANENTLY = 301 +HTTP_302_FOUND = 302 +HTTP_303_SEE_OTHER = 303 +HTTP_304_NOT_MODIFIED = 304 +HTTP_305_USE_PROXY = 305 +HTTP_306_RESERVED = 306 +HTTP_307_TEMPORARY_REDIRECT = 307 +HTTP_308_PERMANENT_REDIRECT = 308 +HTTP_400_BAD_REQUEST = 400 +HTTP_401_UNAUTHORIZED = 401 +HTTP_402_PAYMENT_REQUIRED = 402 +HTTP_403_FORBIDDEN = 403 +HTTP_404_NOT_FOUND = 404 +HTTP_405_METHOD_NOT_ALLOWED = 405 +HTTP_406_NOT_ACCEPTABLE = 406 +HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407 +HTTP_408_REQUEST_TIMEOUT = 408 +HTTP_409_CONFLICT = 409 +HTTP_410_GONE = 410 +HTTP_411_LENGTH_REQUIRED = 411 +HTTP_412_PRECONDITION_FAILED = 412 +HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413 +HTTP_414_REQUEST_URI_TOO_LONG = 414 +HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 +HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416 +HTTP_417_EXPECTATION_FAILED = 417 +HTTP_422_UNPROCESSABLE_ENTITY = 422 +HTTP_423_LOCKED = 423 +HTTP_424_FAILED_DEPENDENCY = 424 +HTTP_426_UPGRADE_REQUIRED = 426 +HTTP_428_PRECONDITION_REQUIRED = 428 +HTTP_429_TOO_MANY_REQUESTS = 429 +HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 +HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451 +HTTP_500_INTERNAL_SERVER_ERROR = 500 +HTTP_501_NOT_IMPLEMENTED = 501 +HTTP_502_BAD_GATEWAY = 502 +HTTP_503_SERVICE_UNAVAILABLE = 503 +HTTP_504_GATEWAY_TIMEOUT = 504 +HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505 +HTTP_506_VARIANT_ALSO_NEGOTIATES = 506 +HTTP_507_INSUFFICIENT_STORAGE = 507 +HTTP_508_LOOP_DETECTED = 508 +HTTP_509_BANDWIDTH_LIMIT_EXCEEDED = 509 +HTTP_510_NOT_EXTENDED = 510 +HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511 diff --git a/GeoIpConfig/dataReader.py b/GeoIpConfig/dataReader.py new file mode 100644 index 0000000..a437ec7 --- /dev/null +++ b/GeoIpConfig/dataReader.py @@ -0,0 +1,24 @@ +import csv +import json +import os.path + +from GeoIpConfig import BASE_DIR + +def read_csv_ip_file(name): + """This function take a csvfile name and return context of it""" + filePath = BASE_DIR.joinpath("GeoIpConfig") / name + with open(file=filePath, encoding="UTF-8", mode="r") as fp: + reader = csv.reader(fp) + reader.__next__() + data = [] + for row in reader: + data.append(row) + return data + + +def read_country_info(): + filePath = BASE_DIR.joinpath("GeoIpConfig/data.json") + with open(file=filePath, encoding="UTF-8", mode="r") as fp: + dt = json.load(fp) + for row in dt: + yield row diff --git a/GeoIpCore/__init__.py b/GeoIpCore/__init__.py new file mode 100644 index 0000000..1552259 --- /dev/null +++ b/GeoIpCore/__init__.py @@ -0,0 +1,34 @@ +from flask import Flask + +from GeoIpConfig import config +from GeoIpCore.extensions import db, cache, migrate, \ + csrf, captchaVersion2, limiter, SessionServer + +def create_app(): + app = Flask(__name__) + app.config.from_object(config) + + # init Extensions + db.init_app(app=app) + cache.init_app(app) + migrate.init_app(app=app, db=db) + csrf.init_app(app) + captchaVersion2.init_app(app) + limiter.init_app(app) + SessionServer.init_app(app) + + from GeoIpApi import api + app.register_blueprint(api, url_prefix="/api/v1/") + + from GeoIpWeb import web + app.register_blueprint(web, url_prefix="/") + + + return app + + +app = create_app() + + +# read Base Views +import GeoIpCore.views diff --git a/GeoIpCore/extensions.py b/GeoIpCore/extensions.py new file mode 100644 index 0000000..e923eab --- /dev/null +++ b/GeoIpCore/extensions.py @@ -0,0 +1,26 @@ +import os +from dotenv import load_dotenv +from flask_session import Session +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_wtf.csrf import CSRFProtect +from flask_captcha2 import FlaskCaptcha2 +from flask_caching import Cache +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address +from redis import Redis + +load_dotenv() +ServerRedis = Redis().from_url(os.environ.get("X_REDIS_URL")) +SessionServer = Session() +db = SQLAlchemy() +migrate = Migrate() +csrf = CSRFProtect() +limiter = Limiter( + get_remote_address, + storage_uri=os.environ.get("X_REDIS_URL"), + storage_options={"socket_connect_timeout": 30}, + strategy="fixed-window", # or "moving-window" +) +cache = Cache() +captchaVersion2 = FlaskCaptcha2() diff --git a/GeoIpCore/model.py b/GeoIpCore/model.py new file mode 100644 index 0000000..8fe25a8 --- /dev/null +++ b/GeoIpCore/model.py @@ -0,0 +1,25 @@ +import uuid +import datetime + +from GeoIpCore.extensions import db +from sqlalchemy import Column, String, DateTime, Integer + +class BaseTable(db.Model): + __abstract__ = True + + id = Column(Integer, primary_key=True) + + def SetPubicKey(self): + while True: + key = str(uuid.uuid4()) + db_res = self.query.filter(self.PublicKey == key).first() + if db_res: + continue + else: + self.PublicKey = key + break + + PublicKey = Column(String(36), nullable=False, unique=True) + CreatedTime = Column(DateTime, default=datetime.datetime.now) + LastUpdateTime = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now) + diff --git a/GeoIpCore/views.py b/GeoIpCore/views.py new file mode 100644 index 0000000..3a29100 --- /dev/null +++ b/GeoIpCore/views.py @@ -0,0 +1,51 @@ +from GeoIpCore import app +from flask import jsonify, request +import GeoIpConfig + +@app.get("/ip/") +def PublicIpAddress(): + """ + ArvanCloud http header sample + - X-Real-Ip: 10.11.7.156 + - Host: www.ip2geo.ir + - X-Forwarded-For: 164.215.236.103, 94.101.182.4, 10.11.7.156 + - X-Forwarded-Proto: https + - X-Forwarded-Port: 80 + - X-Forwarded-Host: www.ip2geo.ir + - Connection: close + - User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 + - Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 + - Accept-Encoding: gzip, deflate, br + - Accept-Language: en-US,en;q=0.5 + - Ar-Real-Country: IR + - Ar-Real-Ip: 164.215.236.103 + - Ar-Sid: 2062 + - Cdn-Loop: arvancloud; count=1 + - Sec-Fetch-Dest: document + - Sec-Fetch-Mode: navigate + - Sec-Fetch-Site: none + - Sec-Fetch-User: ?1 + - True-Client-Ip: 164.215.236.103 + - Upgrade-Insecure-Requests: 1 + - X-Country-Code: IR + - X-Forwarded-Server: server code + - X-Request-Id: some random string + - X-Sid: 2062 + """ + ip = request.headers.get('Ar-Real-Ip', "NULL") + code = request.headers.get('Ar-Real-Country', "NULL") + return jsonify({ + "IPv4": ip if ip else "NULL", + "IPv6": "NULL", + "Country-Code": code if code else "NULL", + "X-Status": "True" if ip and code else "False", + "X-Response-By": "www.ip2geo.ir/ip/" + }) + + +@app.context_processor +def content_template_processor(): + ctx = { + "base_domain": GeoIpConfig.BASE_DOMAIN + } + return ctx \ No newline at end of file diff --git a/GeoIpUpdater/InsertDataByFile.py b/GeoIpUpdater/InsertDataByFile.py new file mode 100644 index 0000000..0aba17d --- /dev/null +++ b/GeoIpUpdater/InsertDataByFile.py @@ -0,0 +1 @@ +import os \ No newline at end of file diff --git a/GeoIpUpdater/__init__.py b/GeoIpUpdater/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/GeoIpUpdater/fetchAndInsertdata.py b/GeoIpUpdater/fetchAndInsertdata.py new file mode 100644 index 0000000..0aba17d --- /dev/null +++ b/GeoIpUpdater/fetchAndInsertdata.py @@ -0,0 +1 @@ +import os \ No newline at end of file diff --git a/GeoIpWeb/__init__.py b/GeoIpWeb/__init__.py new file mode 100644 index 0000000..ed3ec45 --- /dev/null +++ b/GeoIpWeb/__init__.py @@ -0,0 +1,10 @@ +from flask import Blueprint + +web = Blueprint( + "web", + __name__, + static_folder="static", + template_folder="templates" +) + +import GeoIpWeb.views \ No newline at end of file diff --git a/GeoIpWeb/form.py b/GeoIpWeb/form.py new file mode 100644 index 0000000..c7b8727 --- /dev/null +++ b/GeoIpWeb/form.py @@ -0,0 +1,31 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, TextAreaField, SubmitField +from wtforms.validators import Length, DataRequired, InputRequired + + +class ContactUSForm(FlaskForm): + Title = StringField( + validators=[ + DataRequired(), + InputRequired(), + Length(min=6, max=255) + ] + ) + + Email = StringField( + validators=[ + DataRequired(), + InputRequired(), + Length(min=6, max=255) + ] + ) + + Message = TextAreaField( + validators=[ + DataRequired(), + InputRequired(), + Length(min=6, max=512) + ] + ) + + submit = SubmitField() diff --git a/GeoIpWeb/model.py b/GeoIpWeb/model.py new file mode 100644 index 0000000..7b40b3d --- /dev/null +++ b/GeoIpWeb/model.py @@ -0,0 +1,12 @@ +from GeoIpCore.model import BaseTable +from sqlalchemy import Column, String + + +class ContactUS(BaseTable): + __tablename__ = "ContactUs" + + Title = Column(String(255), nullable=False, unique=False) + Email = Column(String(255), nullable=False, unique=False) + Message = Column(String(512), nullable=False, unique=False) + + diff --git a/GeoIpWeb/static/web/css/index.css b/GeoIpWeb/static/web/css/index.css new file mode 100644 index 0000000..c115af5 --- /dev/null +++ b/GeoIpWeb/static/web/css/index.css @@ -0,0 +1,71 @@ +* { + font-family: 'Roboto', sans-serif; +} + +.base-index-background{ + background: #000046; /* fallback for old browsers */ + background: -webkit-linear-gradient(to left, #193e7a, #1d6896); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient(to left, #193e7a, #1d6896); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ +} +.base-index-background{ +background: #1F242A; /* dark grey / +color: #FFFFFF; / white text */ +} +.FadeRightAnimation{ + animation-duration: 1.5s; + animation-name: FadeRight; +}.FadeLeftAnimation{ + animation-duration: 1.5s; + animation-name: FadeLeft; +} + +@keyframes FadeRight{ + 0%{ + transform: translateX(-40px); + opacity: 0; + } + 100%{ + transform: translateX(0px); + opacity: 1; + } +}@keyframes FadeLeft{ + 0%{ + transform: translateX(40px); + opacity: 0; + } + 100%{ + transform: translateX(0px); + opacity: 1; + } + } + + + .answer-faq{ + color: #ccc; + } + .text-gray{ + color: #ccc; + } + +#navbar-burger-menu:hover{ + cursor: pointer; +} +.line1 , .line2, .line3{ + width: 40px; + height: 5px; + margin-top: 6px; + background-color: white; + border-radius: 5px; + transition: transform 0.5s; + opacity: 1; + } + + .line2.hide{ + opacity: 0; + } +.line1.active{ + transform: rotate(45deg) translate(8px, 8px); + } + .line3.active{ + transform: rotate(-45deg) translate(7px, -6px); + } \ No newline at end of file diff --git a/GeoIpWeb/static/web/js/index.js b/GeoIpWeb/static/web/js/index.js new file mode 100644 index 0000000..098e347 --- /dev/null +++ b/GeoIpWeb/static/web/js/index.js @@ -0,0 +1,11 @@ +const burgerMenu = document.querySelector("#navbar-burger-menu") + +const burgerMenuLine1 = document.querySelector(".line1") +const burgerMenuLine2 = document.querySelector(".line2") +const burgerMenuLine3 = document.querySelector(".line3") + +burgerMenu.addEventListener("click", (e)=>{ + burgerMenuLine2.classList.toggle("hide") + burgerMenuLine1.classList.toggle("active") + burgerMenuLine3.classList.toggle("active") +}) \ No newline at end of file diff --git a/GeoIpWeb/static/web/media/51zdsrq20LL.png b/GeoIpWeb/static/web/media/51zdsrq20LL.png new file mode 100644 index 0000000..ffbefea Binary files /dev/null and b/GeoIpWeb/static/web/media/51zdsrq20LL.png differ diff --git a/GeoIpWeb/static/web/media/Feature-Image.jpg b/GeoIpWeb/static/web/media/Feature-Image.jpg new file mode 100644 index 0000000..660c9fa Binary files /dev/null and b/GeoIpWeb/static/web/media/Feature-Image.jpg differ diff --git a/GeoIpWeb/static/web/media/IP-to-Location.jpg b/GeoIpWeb/static/web/media/IP-to-Location.jpg new file mode 100644 index 0000000..ff51885 Binary files /dev/null and b/GeoIpWeb/static/web/media/IP-to-Location.jpg differ diff --git a/GeoIpWeb/static/web/media/IP-to-Location2.jpg b/GeoIpWeb/static/web/media/IP-to-Location2.jpg new file mode 100644 index 0000000..0083961 Binary files /dev/null and b/GeoIpWeb/static/web/media/IP-to-Location2.jpg differ diff --git a/GeoIpWeb/templates/web/base.html b/GeoIpWeb/templates/web/base.html new file mode 100644 index 0000000..33887cd --- /dev/null +++ b/GeoIpWeb/templates/web/base.html @@ -0,0 +1,43 @@ + + + + + + + {% block title %}{% endblock title %} + {% block style %}{% endblock style %} + {% block metatag %}{% endblock metatag %} + + + + + + + + + + + + + + + + + {% block content %} + {% endblock content %} + + {% block script %} + {% endblock script %} + + + + + + + + + + + + \ No newline at end of file diff --git a/GeoIpWeb/templates/web/contactUS.html b/GeoIpWeb/templates/web/contactUS.html new file mode 100644 index 0000000..2fb8477 --- /dev/null +++ b/GeoIpWeb/templates/web/contactUS.html @@ -0,0 +1,62 @@ +{% extends "web/base.html" %} + +{% block title %} + Contact us +{% endblock %} + + +{% block content %} + {% include "web/utils/navbar.html" %} + +
+
+
+
+
+

Contact US

+
+ {{ form.hidden_tag() }} +
+ + {{ form.Title(id="Title", class="form-control py-2 m-0", placeholder="Title / Name") }} + {% if form.Title.errors %} + {% for err in form.Title.errors %} +
  • {{ err }}
  • + {% endfor %} + {% endif %} +
    +
    + + {{ form.Email(id="Email", class="form-control py-2 m-0", placeholder="Email Address") }} + {% if form.Email.errors %} + {% for err in form.Email.errors %} +
  • {{ err }}
  • + {% endfor %} + {% endif %} +
    +
    + + {{ form.Message(rows="10" ,id="Message", class="form-control py-2 m-0", placeholder="Message") }} + {% if form.Message.errors %} + {% for err in form.Message.errors %} +
  • {{ err }}
  • + {% endfor %} + {% endif %} +
    +
    + {{ captchaField }} +
    +
    + {{ form.submit(value="Send", class="btn btn-warning px-5") }} +
    +
    +
    +
    +
    + + {% include "web/utils/footer.html" %} + {% include "web/utils/alert.html" %} +{% endblock %} + +{% block script %} +{% endblock %} \ No newline at end of file diff --git a/GeoIpWeb/templates/web/docs/IP2Location.html b/GeoIpWeb/templates/web/docs/IP2Location.html new file mode 100644 index 0000000..bd51280 --- /dev/null +++ b/GeoIpWeb/templates/web/docs/IP2Location.html @@ -0,0 +1,323 @@ +{% extends "web/base.html" %} + +{% block title %} +IP to Geo Location +{% endblock %} + + +{% block content %} + {% include "web/utils/navbar.html" %} + +
    +
    +
    +
    +
    + +
    +

    + IP to Location +

    +
    +
    +

    +An IP-to-location service is a type of technology that helps determine the physical location of a particular device based on its IP address. Every device connected to the internet is assigned a unique IP address, and an IP-to-location service can use this information to accurately identify the general geographic location of that device. This technology can be useful for a variety of purposes, such as targeted advertising, fraud detection, and geographically restricted content delivery. +

    +
    +

    notice: All of our Endpoints Have a rate limit of 60 request per minute

    +
    +
    + + +
    +
    +

    this View Support IPV4 and IPV6 as well

    +

    +
    +
    + +
    +

    Url schema:

    +
    {{ base_domain  }}/api/v1/IP version/IP address/[true optional]
    +
    + +
    +

    This Endpoint return Your public IP address in JSON format

    +

    Switches: if you pass /true in the end of url view return full info about ip's country

    + +
    + +
    +
    {{ base_domain  }}/api/v1/ipv4/8.8.8.8/true
    +
    {{ base_domain  }}/api/v1/ipv6/::ffff:100:100/true
    +
    + +
    +
    {
    +  "City": "Mountain View",
    +  "Country": {
    +    "name": "United States of America"
    +  },
    +  "CountryCode": "US",
    +  "IPv4": {
    +    "decimal": 134744072,
    +    "hex": "0x8080808",
    +    "octet": "8.8.8.8"
    +  },
    +  "Latitude": "37.405992",
    +  "Longitude": "-122.078515",
    +  "State": "California",
    +  "TimeZone": "-07:00",
    +  "ZipCode": "94043",
    +  "x-Response-BY": "{{ base_domain  }}/api/v1/ipv4/8.8.8.8"
    +}
    +
    + + +
    +

    Full Answer Look like this:

    +
    +
    +
    {{ base_domain  }}/api/v1/ipv4/8.8.8.8/true
    +
    + +
    +
    {
    +  "City": "Mountain View",
    +  "Country": {
    +    "capital": [
    +      "Washington, D.C."
    +    ],
    +    "capitalInfo": {
    +      "latlng": [
    +        38.89,
    +        -77.05
    +      ]
    +    },
    +    "continents": [
    +      "North America"
    +    ],
    +    "flag": "🇺🇸",
    +    "flags": {
    +      "alt": "The flag of the United States of America is composed of thirteen equal horizontal bands of red alternating with white. A blue rectangle, bearing fifty small five-pointed white stars arranged in nine rows where rows of six stars alternate with rows of five stars, is superimposed in the canton.",
    +      "png": "https://flagcdn.com/w320/us.png",
    +      "svg": "https://flagcdn.com/us.svg"
    +    },
    +    "languages": {
    +      "eng": "English"
    +    },
    +    "maps": {
    +      "googleMaps": "https://goo.gl/maps/e8M246zY4BSjkjAv6",
    +      "openStreetMaps": "https://www.openstreetmap.org/relation/148838#map=2/20.6/-85.8"
    +    },
    +    "name": "United States of America",
    +    "nativeName": {
    +      "eng": {
    +        "common": "United States",
    +        "official": "United States of America"
    +      }
    +    },
    +    "officialName": "United States of America",
    +    "region": "Americas",
    +    "subregion": "North America",
    +    "translations": {
    +      "ara": {
    +        "common": "الولايات المتحدة",
    +        "official": "الولايات المتحدة الامريكية"
    +      },
    +      "bre": {
    +        "common": "Stadoù-Unanet",
    +        "official": "Stadoù-Unanet Amerika"
    +      },
    +      "ces": {
    +        "common": "Spojené státy",
    +        "official": "Spojené státy americké"
    +      },
    +      "cym": {
    +        "common": "United States",
    +        "official": "United States of America"
    +      },
    +      "deu": {
    +        "common": "Vereinigte Staaten",
    +        "official": "Vereinigte Staaten von Amerika"
    +      },
    +      "est": {
    +        "common": "Ameerika Ühendriigid",
    +        "official": "Ameerika Ühendriigid"
    +      },
    +      "fin": {
    +        "common": "Yhdysvallat",
    +        "official": "Amerikan yhdysvallat"
    +      },
    +      "fra": {
    +        "common": "États-Unis",
    +        "official": "Les états-unis d'Amérique"
    +      },
    +      "hrv": {
    +        "common": "Sjedinjene Američke Države",
    +        "official": "Sjedinjene Države Amerike"
    +      },
    +      "hun": {
    +        "common": "Amerikai Egyesült Államok",
    +        "official": "Amerikai Egyesült Államok"
    +      },
    +      "ita": {
    +        "common": "Stati Uniti d'America",
    +        "official": "Stati Uniti d'America"
    +      },
    +      "jpn": {
    +        "common": "アメリカ合衆国",
    +        "official": "アメリカ合衆国"
    +      },
    +      "kor": {
    +        "common": "미국",
    +        "official": "아메리카 합중국"
    +      },
    +      "nld": {
    +        "common": "Verenigde Staten",
    +        "official": "Verenigde Staten van Amerika"
    +      },
    +      "per": {
    +        "common": "ایالات متحده آمریکا",
    +        "official": "ایالات متحده آمریکا"
    +      },
    +      "pol": {
    +        "common": "Stany Zjednoczone",
    +        "official": "Stany Zjednoczone Ameryki"
    +      },
    +      "por": {
    +        "common": "Estados Unidos",
    +        "official": "Estados Unidos da América"
    +      },
    +      "rus": {
    +        "common": "Соединённые Штаты Америки",
    +        "official": "Соединенные Штаты Америки"
    +      },
    +      "slk": {
    +        "common": "Spojené štáty americké",
    +        "official": "Spojené štáty Americké"
    +      },
    +      "spa": {
    +        "common": "Estados Unidos",
    +        "official": "Estados Unidos de América"
    +      },
    +      "srp": {
    +        "common": "Сједињене Америчке Државе",
    +        "official": "Сједињене Америчке Државе"
    +      },
    +      "swe": {
    +        "common": "USA",
    +        "official": "Amerikas förenta stater"
    +      },
    +      "tur": {
    +        "common": "Amerika Birleşik Devletleri",
    +        "official": "Amerika Birleşik Devletleri"
    +      },
    +      "urd": {
    +        "common": "ریاستہائے متحدہ",
    +        "official": "ریاستہائے متحدہ امریکا"
    +      },
    +      "zho": {
    +        "common": "美国",
    +        "official": "美利坚合众国"
    +      }
    +    }
    +  },
    +  "CountryCode": "US",
    +  "IPv4": {
    +    "decimal": 134744072,
    +    "hex": "0x8080808",
    +    "octet": "8.8.8.8"
    +  },
    +  "Latitude": "37.405992",
    +  "Longitude": "-122.078515",
    +  "State": "California",
    +  "TimeZone": "-07:00",
    +  "ZipCode": "94043",
    +  "tld": [
    +    ".us"
    +  ],
    +  "x-Response-BY": "{{ base_domain  }}/api/v1/ipv4/8.8.8.8"
    +}
    +
    + +
    +

    How to Use:

    +
    + + +
    +

    Curl

    +
    curl https://ip2geo.ip/api/v1/8.8.8.8
    +
    + +
    +

    Python

    +
    import requests
    +response = requests.get("{{ base_domain  }}/api/v1/ipv4/8.8.8.8")
    +print(response.json())
    +
    + +
    +

    C#

    +
    using System.Net.Http;
    +
    +using var client = new HttpClient();
    +using var response = await client.GetAsync("{{ base_domain  }}/api/v1/ipv4/8.8.8.8");
    +var content = await response.Content.ReadAsStringAsync();
    +Console.WriteLine(content);
    +
    + + +
    +

    PHP

    +
    $url = "{{ base_domain  }}/api/v1/ipv4/8.8.8.8";
    +$ch = curl_init($url);
    +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    +$result = curl_exec($ch);
    +curl_close($ch);
    +$json_data = json_decode($result, true);
    +
    + +
    +

    JavaScript

    +
    fetch('{{ base_domain  }}/api/v1/ipv4/8.8.8.8', {
    +  method: 'GET'
    +})
    +.then(response => response.json())
    +.then(jsonData => {
    +  console.log(jsonData);
    +})
    +.catch((Exception) => {
    +  console.error(Exception);
    +});
    +
    + +
    +

    Javascript - Jquery

    +
    $.get( "{{ base_domain  }}/api/v1/ipv4/8.8.8.8", function (response) {
    +  console.log(response)
    +});
    +
    + + +
    +
    +
    + +
    +
    + + {% include "web/utils/footer.html" %} + {% include "web/utils/alert.html" %} +{% endblock %} + +{% block script %} + +{% endblock %} \ No newline at end of file diff --git a/GeoIpWeb/templates/web/docs/PublicIP.html b/GeoIpWeb/templates/web/docs/PublicIP.html new file mode 100644 index 0000000..a638dbb --- /dev/null +++ b/GeoIpWeb/templates/web/docs/PublicIP.html @@ -0,0 +1,127 @@ +{% extends "web/base.html" %} + +{% block title %} +Public IP Address +{% endblock %} + + +{% block content %} + {% include "web/utils/navbar.html" %} + +
    +
    +
    +
    +
    + +
    +

    + Public IP address +

    +
    +
    +

    + A public IP address is a unique identifier assigned to a device that is connected to the Internet. This address allows other devices on the Internet to communicate with the device that has been assigned the public IP address. Public IP addresses are typically provided by an Internet Service Provider (ISP) and can be either dynamic, meaning they can change over time, or static, meaning they remain the same for an extended period of time. Public IP addresses are different from private IP addresses, which are used to identify devices on a local network. +

    +
    +

    notice: All of our Endpoints Have a rate limit of 60 request per minute

    +
    +
    + + +
    +
    {{ base_domain }}/ip/
    +
    +
    +

    This Endpoint return Your public IP address in JSON format

    +
    +
    +
    {
    +  "Country-Code": "IR",
    +  "IPv4": "164.215.236.103",
    +  "IPv6": "NULL",
    +  "X-status": "True",
    +  "x-Response-By": "www.ip2geo.ir/ip/"
    +}
    +
    + +
    +

    How to Use:

    +
    + +
    +

    Curl

    +
    curl https://ip2geo.ip/ip/
    +
    + + +
    +

    Python

    +
    import requests
    +response = requests.get("{{ base_domain }} /ip/")
    +print(response.json())
    +
    + +
    +

    C#

    +
    using System.Net.Http;
    +
    +using var client = new HttpClient();
    +using var response = await client.GetAsync("{{ base_domain }} /ip/");
    +var content = await response.Content.ReadAsStringAsync();
    +Console.WriteLine(content);
    +
    + + +
    +

    PHP

    +
    $url = "{{ base_domain }} /ip/";
    +$ch = curl_init($url);
    +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    +$result = curl_exec($ch);
    +curl_close($ch);
    +$json_data = json_decode($result, true);
    +
    + +
    +

    JavaScript

    +
    fetch('{{ base_domain }} /ip/', {
    +  method: 'GET'
    +})
    +.then(response => response.json())
    +.then(jsonData => {
    +  console.log(jsonData);
    +})
    +.catch((Exception) => {
    +  console.error(Exception);
    +});
    +
    + +
    +

    Javascript - Jquery

    +
    $.get( "{{ base_domain }} /ip/", function (response) {
    +  console.log(response)
    +});
    +
    + + +
    +
    +
    + +
    +
    + + {% include "web/utils/footer.html" %} + {% include "web/utils/alert.html" %} +{% endblock %} + +{% block script %} + +{% endblock %} \ No newline at end of file diff --git a/GeoIpWeb/templates/web/index.html b/GeoIpWeb/templates/web/index.html new file mode 100644 index 0000000..299253a --- /dev/null +++ b/GeoIpWeb/templates/web/index.html @@ -0,0 +1,166 @@ +{% extends "web/base.html" %} + + +{% block title %} + IP 2 Geo Location +{% endblock title %} + +{% block style %} +{% endblock style %} + +{% block content %} + {% include "web/utils/navbar.html" %} +
    +
    + +
    +
    + +
    +

    IP 2 Geo Location

    +

    Fast, Free,Easy to use, and Open Source

    +

    Easy to integrate, Available in JSON

    +
    + +
    +
    +
    +
    +
    +{
    +  "City": "Mountain View",
    +  "Country": {
    +    "name": "United States of America"
    +  },
    +  "CountryCode": "US",
    +  "IPv4": {
    +    "decimal": 134744072,
    +    "hex": "0x8080808",
    +    "octet": "8.8.8.8"
    +  },
    +  "Latitude": "37.405992",
    +  "Longitude": "-122.078515",
    +  "State": "California",
    +  "TimeZone": "-07:00",
    +  "ZipCode": "94043",
    +  "x-Response-BY": "https://ip2geo.ir/api/v1/ipv4/8.8.8.8"
    +}
    +
    +
    + +
    + + +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    Frequently Asked Questions
    +
    +
    +
    + +
    +

    Is your database and information reliable and up-to-date?

    +

    +Yes, we update our databases daily and automatically to provide the most accurate and reliable information possible. we also use ip2location free data bases as well +

    +
    +
    +

    Can I use your API for commercial products?

    +

    + Yes, we update our databases daily and automatically to provide the most accurate and reliable information possible. +

    +
    +
    +

    Is it possible to access IP history information?

    +

    + No, it's not possible to access request and API history information as we don't keep this data to protect the privacy of our users. +

    +
    +
    +

    Do I need to pay to use your API?

    +

    + No, there is no cost to use the services provided by us, and all of these services are available for free. +

    +
    +
    +

    Can I send more than 60 requests per minute?

    +

    + No, there is no cost to use the services provided by us, and all of these services are available for free. +

    +
    +
    +

    Do I need an API key to make a request?

    +

    + Generally, no. All endpoints are open and accessible for free. However, we have implemented restrictions on requests to manage the volume of traffic. Each endpoint can respond to a maximum of 60 requests per minute. + If you exceed the limit of 60 requests per minute, you will receive a 429 error and will need to wait until your 1-minute restriction has ended. +

    +
    +
    +
    +
    +
    +
    + + + + +
    +
    +
    +
    +

    Services That We Provide

    +
    +
    +
    +
    +

    IP to GeoLocation Service

    +

    + This Service helps you to get location of an ip address +

    +
    +
    +

    Countries Information

    +

    + This service can help you for getting a country full information in json format +

    +
    +
    +

    DNS Servers

    +

    + This service can help you for getting a full list of a countries DNS servers +

    +
    +
    +

    Uptime server

    +

    + This service can help you for monitoring your services +

    +
    + +
    +
    +
    +
    +
    + + + + + {% include "web/utils/footer.html" %} +{% endblock content %} + +{% block script %} +{% endblock script %} diff --git a/GeoIpWeb/templates/web/privacy.html b/GeoIpWeb/templates/web/privacy.html new file mode 100644 index 0000000..db9b9ec --- /dev/null +++ b/GeoIpWeb/templates/web/privacy.html @@ -0,0 +1,47 @@ +{% extends "web/base.html" %} + + +{% block title %} + Privacy/Terms of Use +{% endblock title %} + +{% block style %} +{% endblock style %} + +{% block content %} + {% include "web/utils/navbar.html" %} + +
    +
    +
    +
    +

    Privacy/Terms of Use

    +
    + +
    +

    Security

    +

    We do not store or maintain any information about API requests anywhere.

    +
    +
    +

    Data and Security

    +

    We make every effort to provide accurate and up-to-date information to you, and we also strive to maintain the security of our users.

    +
    +
    +

    Cost and services

    +

    Our service and offerings are free for the general public, and there is no cost required to use our service

    +
    + +
    +
    +
    + + + + + + + {% include "web/utils/footer.html" %} +{% endblock content %} + +{% block script %} +{% endblock script %} diff --git a/GeoIpWeb/templates/web/utils/alert.html b/GeoIpWeb/templates/web/utils/alert.html new file mode 100644 index 0000000..d085d8b --- /dev/null +++ b/GeoIpWeb/templates/web/utils/alert.html @@ -0,0 +1,17 @@ + +{% with messages = get_flashed_messages(True) %} + {% if messages %} + {% for cat, msg in messages %} +
    +
    + +
    +
    +
    +

    {{msg}}

    +
    +
    + {% endfor %} + {% endif %} +{% endwith %} + \ No newline at end of file diff --git a/GeoIpWeb/templates/web/utils/footer.html b/GeoIpWeb/templates/web/utils/footer.html new file mode 100644 index 0000000..312d891 --- /dev/null +++ b/GeoIpWeb/templates/web/utils/footer.html @@ -0,0 +1,35 @@ +
    + +
    +
    +
    +
    +
    + +
    IP 2 Geo Location/Free to Everyone
    +

    © 2023 ip2geo.ir. All rights reserved.

    +
    +
    +
    Terms of Use
    + Privacy + Contact uS +

    About us

    +
    +
    +
    Social Networks
    +
    + + + + + + + +
    +
    +
    +
    +
    +
    + +
    diff --git a/GeoIpWeb/templates/web/utils/navbar.html b/GeoIpWeb/templates/web/utils/navbar.html new file mode 100644 index 0000000..72fda59 --- /dev/null +++ b/GeoIpWeb/templates/web/utils/navbar.html @@ -0,0 +1,70 @@ + + + + + \ No newline at end of file diff --git a/GeoIpWeb/views.py b/GeoIpWeb/views.py new file mode 100644 index 0000000..18252a7 --- /dev/null +++ b/GeoIpWeb/views.py @@ -0,0 +1,83 @@ +import ipaddress +from flask import render_template, send_from_directory, flash, redirect, request, jsonify + +from GeoIpWeb import web +from GeoIpConfig import BASE_DIR +from GeoIpWeb.form import ContactUSForm +from GeoIpWeb.model import ContactUS as ContactUsModel +from GeoIpCore.extensions import captchaVersion2, db, limiter,\ + cache + + + +@web.get("/webStatic/") +def WebStatic(path): + return send_from_directory(BASE_DIR.joinpath("GeoIpWeb/static"), path) + +@web.get("/") +@cache.cached(3600*8) +def index_view(): + return render_template("web/index.html") + + +@web.get("/contact-us/") +def contactUS(): + form = ContactUSForm() + return render_template("web/contactUS.html", form=form) + + +@web.post("/contact-us/") +def contactUSPost(): + """ + this view take a post request for sibmitting a contact us form + """ + form = ContactUSForm() + if not form.validate(): + flash("some data are missing!", "danger") + return render_template("web/contactUS.html", form=form) + if not captchaVersion2.is_verify(): + flash("Captcha is incorrect!", "danger") + return render_template("web/contactUS.html", form=form) + + contact_us = ContactUsModel() + contact_us.SetPubicKey() + contact_us.Email = form.Email.data + contact_us.Title = form.Title.data + contact_us.Message = form.Message.data + try: + db.session.add(contact_us) + db.session.commit() + except Exception as e: + print(e) + db.session.rollback() + flash('Error 46', 'danger') + return render_template("web/contactUS.html", form=form) + else: + flash('Form Submitted successfully', 'success') + return redirect(request.referrer) + +@web.get("/ip/") +@limiter.limit("60 per minute") +def PublicIpAddress(): + ip = request.headers.get('X-Real-IP', "NULL") + return jsonify({ + "IPv4":ip if ip else "NULL", + "IPv6":"NULL", + "X-status":"X-Real-IP False", + "x-Response-By": "www.ip2geo.ir" + }) + +@web.get("/doc/Public-IP-Address/") +@cache.cached(43200) +def doc_public_ip_address(): + return render_template("web/docs/PublicIP.html") + +@web.get("/doc/IP-to-Location/") +@cache.cached(43200) +def doc_ip_to_location(): + return render_template("web/docs/IP2Location.html") + +@web.get("/Privacy-Terms-of-Use/") +@cache.cached(43200) +def privacy_term_of_use(): + return render_template("web/privacy.html") \ No newline at end of file diff --git a/MakeMigrate b/MakeMigrate new file mode 100644 index 0000000..9c6381a --- /dev/null +++ b/MakeMigrate @@ -0,0 +1,5 @@ +#! usr/bin/sh + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..7706030 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# GeoIP-API +### api service for converting IP addresses to Location + + + + + + + + +### Tech stack: + + Python3 + Flask 2.3x + Flask-wtf + Flask-Sqlalchemy + Flask-Migrate + Flask-Caching + Flask-Limiter + Flask-Captcha2 + Flask-Cors + Redis + + + + + +## how to run : + +### 0.0 create a virtual env + python -m venv venv + + +### 0.1 activate virtual env + + linux-mac : + source ./venv/bin/active + windows: + ./venv/Scripts/activate + + +### 0.2 install dependency: + + pip install -r requirements.txt + +### 0.3 change.env file + + mv .env.sample .env + then change data in .env file (like data base name, ...) + +### migrate to db + + flask db migrate + flask db upgrade + + +### run App + + python app.py + or + flask run [--debug(for debug) --reload(reload template) --port 8080(for port)] + [...] is optional + +--- + +### at this point web app is up and running +but there is no data in database so let add some data to database + + +## warning : before running any of below script you should first fill up .env file and also migrate changes to db ! +### 0.0 for adding automatically data + + cd ./GeoIpUpdater + python fetchAndInsertdata.py + + - this script automatically fetch a dataset from github + and then update database with new data + + + +### 0.1 just insert data +#### if you have data your self just run below script + + cd ./GeoIpUpdater + python InsertDataByFile.py + + - this script insert data to database base on an input file ( file is required ) + + + + +## Deploy to Server: + +### This Web App configure for Deploy to liara.ir - if you want to deploy to other Pass Service providers make sure to change configuration base of service provider that you use. + +#### For See How Deploy To liara.ir see Here + diff --git a/app.py b/app.py new file mode 100644 index 0000000..5059300 --- /dev/null +++ b/app.py @@ -0,0 +1,5 @@ +from GeoIpCore import app + + +if __name__ == "__main__": + app.run() diff --git a/dev.txt b/dev.txt new file mode 100644 index 0000000..3017c1a --- /dev/null +++ b/dev.txt @@ -0,0 +1,8 @@ +todo: + + GeoUpdater scripts + background Task + Add User Panel + Add API-KEY for special EndPoints + + \ No newline at end of file diff --git a/doc/Deploy/liara.ir/README b/doc/Deploy/liara.ir/README new file mode 100644 index 0000000..117f5e6 --- /dev/null +++ b/doc/Deploy/liara.ir/README @@ -0,0 +1,3 @@ +# for deploy to liara first create a flask app with a unique name + +# then go to /liara.json and change name property to your app name then just run liara deploy ... diff --git a/doc/Images/index.png b/doc/Images/index.png new file mode 100644 index 0000000..cb61690 Binary files /dev/null and b/doc/Images/index.png differ diff --git a/doc/Images/ip2location.png b/doc/Images/ip2location.png new file mode 100644 index 0000000..7257792 Binary files /dev/null and b/doc/Images/ip2location.png differ diff --git a/doc/Images/public-ip.png b/doc/Images/public-ip.png new file mode 100644 index 0000000..89b2ec3 Binary files /dev/null and b/doc/Images/public-ip.png differ diff --git a/liara.json b/liara.json new file mode 100644 index 0000000..8821bf6 --- /dev/null +++ b/liara.json @@ -0,0 +1,11 @@ + +{ + "flask": { + "pythonVersion": "3.11" + }, + + "platform": "flask", + "app": "geopip" + + +} diff --git a/liara_nginx.conf b/liara_nginx.conf new file mode 100644 index 0000000..3b997e1 --- /dev/null +++ b/liara_nginx.conf @@ -0,0 +1,23 @@ +gzip on; +gzip_disable "msie6"; +gzip_vary on; +gzip_proxied any; +gzip_comp_level 6; +gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + + +location / { + try_files /dev/null @flask_app; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} + + +location ~\.sqlite3$ { + deny all; + error_page 403 =404 /; +} + +location ~ /\.well-known { + allow all; +} \ No newline at end of file diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..89f80b2 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,110 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except TypeError: + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/8624cbb5fd4e_.py b/migrations/versions/8624cbb5fd4e_.py new file mode 100644 index 0000000..a2e00a8 --- /dev/null +++ b/migrations/versions/8624cbb5fd4e_.py @@ -0,0 +1,91 @@ +"""empty message + +Revision ID: 8624cbb5fd4e +Revises: +Create Date: 2023-07-20 19:33:58.692839 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8624cbb5fd4e' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('ContactUs', + sa.Column('Title', sa.String(length=255), nullable=False), + sa.Column('Email', sa.String(length=255), nullable=False), + sa.Column('Message', sa.String(length=512), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('PublicKey', sa.String(length=36), nullable=False), + sa.Column('CreatedTime', sa.DateTime(), nullable=True), + sa.Column('LastUpdateTime', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('PublicKey') + ) + op.create_table('CountryInfo', + sa.Column('CommonName', sa.String(length=255), nullable=False), + sa.Column('OfficialName', sa.String(length=255), nullable=False), + sa.Column('CountryCode', sa.String(length=64), nullable=False), + sa.Column('Info', sa.JSON(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('PublicKey', sa.String(length=36), nullable=False), + sa.Column('CreatedTime', sa.DateTime(), nullable=True), + sa.Column('LastUpdateTime', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('PublicKey') + ) + op.create_table('IPV4', + sa.Column('StartRange', sa.BIGINT(), nullable=False), + sa.Column('EndRange', sa.BIGINT(), nullable=False), + sa.Column('CountryCode', sa.String(length=64), nullable=False), + sa.Column('CountryName', sa.String(length=64), nullable=False), + sa.Column('StateName', sa.String(length=64), nullable=False), + sa.Column('CityName', sa.String(length=64), nullable=False), + sa.Column('Lat', sa.String(length=64), nullable=False), + sa.Column('Long', sa.String(length=64), nullable=False), + sa.Column('ZipCode', sa.String(length=64), nullable=False), + sa.Column('TimeZone', sa.String(length=64), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('PublicKey', sa.String(length=36), nullable=False), + sa.Column('CreatedTime', sa.DateTime(), nullable=True), + sa.Column('LastUpdateTime', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('EndRange'), + sa.UniqueConstraint('PublicKey') + ) + op.create_table('IPV6', + sa.Column('StartRange', sa.DECIMAL(precision=36, scale=0), nullable=False), + sa.Column('EndRange', sa.DECIMAL(precision=36, scale=0), nullable=False), + sa.Column('CountryCode', sa.String(length=64), nullable=False), + sa.Column('CountryName', sa.String(length=64), nullable=False), + sa.Column('StateName', sa.String(length=64), nullable=False), + sa.Column('CityName', sa.String(length=64), nullable=False), + sa.Column('Lat', sa.String(length=64), nullable=False), + sa.Column('Long', sa.String(length=64), nullable=False), + sa.Column('ZipCode', sa.String(length=64), nullable=False), + sa.Column('TimeZone', sa.String(length=64), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('PublicKey', sa.String(length=36), nullable=False), + sa.Column('CreatedTime', sa.DateTime(), nullable=True), + sa.Column('LastUpdateTime', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('EndRange'), + sa.UniqueConstraint('PublicKey') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('IPV6') + op.drop_table('IPV4') + op.drop_table('CountryInfo') + op.drop_table('ContactUs') + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..69c3cbc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,61 @@ +alembic==1.11.1 +asttokens==2.2.1 +async-timeout==4.0.2 +backcall==0.2.0 +blinker==1.6.2 +cachelib==0.9.0 +certifi==2023.5.7 +charset-normalizer==3.1.0 +click==8.1.3 +colorama==0.4.6 +decorator==5.1.1 +Deprecated==1.2.14 +executing==1.2.0 +Flask==2.3.2 +Flask-Cache==0.13.1 +Flask-Caching==2.0.2 +Flask-captcha2==2.0.1 +Flask-Cors==3.0.10 +Flask-Limiter==3.3.1 +Flask-Migrate==4.0.4 +Flask-Session==0.5.0 +flask-shell-ipython==0.5.1 +Flask-SQLAlchemy==3.0.4 +Flask-WTF==1.1.1 +greenlet==2.0.2 +idna==3.4 +importlib-resources==5.12.0 +ipython==8.14.0 +itsdangerous==2.1.2 +jedi==0.18.2 +Jinja2==3.1.2 +limits==3.5.0 +Mako==1.2.4 +markdown-it-py==3.0.0 +MarkupSafe==2.1.3 +matplotlib-inline==0.1.6 +mdurl==0.1.2 +ordered-set==4.1.0 +packaging==23.1 +parso==0.8.3 +pexpect==4.8.0 +pickleshare==0.7.5 +prompt-toolkit==3.0.39 +ptyprocess==0.7.0 +pure-eval==0.2.2 +Pygments==2.15.1 +PyMySQL==1.0.3 +python-dotenv==1.0.0 +redis==4.5.5 +requests==2.31.0 +rich==13.4.2 +six==1.16.0 +SQLAlchemy==2.0.16 +stack-data==0.6.2 +traitlets==5.9.0 +typing_extensions==4.6.3 +urllib3==2.0.3 +wcwidth==0.2.6 +Werkzeug==2.3.6 +wrapt==1.15.0 +WTForms==3.0.1