├── .gitignore ├── .python-version ├── LICENSE ├── README.md ├── SECURITY.md ├── lambda ├── .gitignore ├── Makefile ├── README.md ├── curator.py ├── downloader.py ├── parser.py ├── requirements.txt ├── test_local_curator.py ├── test_local_downloader.py └── test_local_parser.py ├── legacy ├── README.md ├── RIR-downloader.sh ├── SEF-parser.py ├── cc2asn-server.py ├── cc2asn.conf ├── cc2asn.upstart └── install.sh └── misc ├── CC2ASN-Arch-Dark.drawio ├── CC2ASN-Arch-Dark.png ├── CC2ASN-Arch-Light.drawio └── CC2ASN-Arch-Light.png /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | cc2asn-λ39 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tor Inge Skaar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CC2ASN 2 | ====== 3 | 4 | A lookup service for AS-numbers and prefixes belonging to any given country in 5 | the world. Simply provide country codes as input and you’ll get all ASNs, IPv4 6 | or IPv6 addresses registered to that country. For more detailed information 7 | check out the website at [www.cc2asn.com][1]. 8 | 9 | > **Note** 10 | > If you're only interested in the CC2ASN data simply use the service as 11 | > described on [cc2asn.com][1] 12 | 13 | This is the redesigned event driven architecure for CC2ASN. If you are looking 14 | for the legacy server based version, then go [here](legacy/README.md) 15 | 16 | Architecture 17 | ------------ 18 | ![Architecture Diagram](misc/CC2ASN-Arch-Dark.png#gh-dark-mode-only) 19 | ![Architecture Diagram](misc/CC2ASN-Arch-Light.png#gh-light-mode-only) 20 | 21 | [1]: http://www.cc2asn.com 22 | 23 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This is not a product or software on a regular release cadence, but primarily 4 | a code repository for the services I run on cc2asn.com. That said, I'm truly 5 | greatful for any feedback on security related issues or concerns. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Best way of reporting a security issue is to create a new issue here on github. 10 | That provides transparency and I will be notified directly. 11 | -------------------------------------------------------------------------------- /lambda/.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | *.bkp 3 | __pycache__ 4 | .DS_Store/* 5 | -------------------------------------------------------------------------------- /lambda/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = CC2ASN 2 | 3 | $(eval deploy:;@:) 4 | 5 | .PHONY: downloader parser curator 6 | 7 | downloader parser curator: 8 | ifeq (deploy, $(filter deploy,$(MAKECMDGOALS))) 9 | @echo 'Deploy $@' 10 | $(eval FUNCTION := $(PROJECT)-$@) 11 | $(eval REGION := $(shell pcregrep -o1 "^REGION = \"([a-z]{2}-[a-z]*-\d)\"" $@.py)) 12 | cp requirements.txt requirements.bkp 2>/dev/null || : 13 | pip3 freeze > requirements.txt 14 | rm -rf build 15 | mkdir -p build/site-packages 16 | zip -r build/$(FUNCTION).zip $@.py 17 | cp -r $$VIRTUAL_ENV/lib/python3.*/site-packages/ build/site-packages 18 | cd build/site-packages; zip -g -r ../$(FUNCTION).zip . -x "*__pycache__*" 19 | aws lambda update-function-code \ 20 | --region=$(REGION) \ 21 | --function-name $(FUNCTION) \ 22 | --zip-file fileb://build/$(FUNCTION).zip \ 23 | --publish 24 | else 25 | @echo 'Build $@ package' 26 | $(eval FUNCTION := $(PROJECT)-$@) 27 | cp requirements.txt requirements.bkp 2>/dev/null || : 28 | pip3 freeze > requirements.txt 29 | rm -rf build 30 | mkdir -p build/site-packages 31 | zip -r build/$(FUNCTION).zip $@.py 32 | cp -r $$VIRTUAL_ENV/lib/python3.*/site-packages/ build/site-packages 33 | cd build/site-packages; zip -g -r ../$(FUNCTION).zip . -x "*__pycache__*" 34 | endif 35 | 36 | all: downloader parser curator 37 | 38 | clean: 39 | rm -rf build -------------------------------------------------------------------------------- /lambda/README.md: -------------------------------------------------------------------------------- 1 | `make [deploy] ` 2 | 3 | `make downloader` will build a package for the downloader function 4 | 5 | `make deploy parser` will build a package and deploy the parser function 6 | 7 | `make deploy all` will package and deploy all three functions 8 | 9 | `make clean` will remove the build sub-directory 10 | 11 | -------------------------------------------------------------------------------- /lambda/curator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import logging 4 | from datetime import datetime 5 | 6 | import boto3 7 | import natsort 8 | 9 | # AWS configuration 10 | REGION = "eu-west-1" # AWS region name 11 | BUCKET = "cc2asn-db" # S3 bucket for curated data 12 | 13 | # Setup logging 14 | logger = logging.getLogger(__name__) 15 | 16 | # Function that reads a file from S3 and returns its content 17 | def read_s3_file(bucket, key): 18 | s3 = boto3.resource(service_name="s3", region_name=REGION) 19 | try: 20 | obj = s3.Object(bucket, key) 21 | data = obj.get()["Body"].read().decode("utf-8") 22 | logger.info(f"Successfully read {bucket}/{key}") 23 | except Exception as e: 24 | logger.error(f"Failed to read {bucket}/{key}") 25 | logger.exception(e) 26 | data = None 27 | return data 28 | 29 | 30 | # Store structure data to S3: 31 | def dbstore(ccdata): 32 | s3 = boto3.resource(service_name="s3", region_name=REGION) 33 | if "DATE" in ccdata: 34 | # Get and remove the generation date 35 | yyyy = ccdata["DATE"][:4] 36 | mm = ccdata["DATE"][4:6] 37 | dd = ccdata["DATE"][6:8] 38 | try: 39 | datetime(int(yyyy), int(mm), int(dd)) 40 | usedate = True 41 | logger.debug(f"RIR generation date: {yyyy}-{mm}-{dd}") 42 | except ValueError: 43 | logger.error("Invalid RIR generation date") 44 | usedate = False 45 | del ccdata["DATE"] 46 | else: 47 | logger.warning("No RIR generation date found") 48 | usedate = False 49 | 50 | fc = 0 51 | # For each country in the dataset 52 | for cc in ccdata: 53 | logger.debug(f"Processing country: {cc}") 54 | if not cc: 55 | continue 56 | files = {} 57 | alldata = [] 58 | typedata = ccdata[cc] 59 | types = sorted(typedata.keys()) 60 | 61 | # Create one file for each record type 62 | for rectype in types: 63 | if rectype == "IPV6": 64 | data = sorted(typedata[rectype]) 65 | else: 66 | data = natsort.natsorted(typedata[rectype]) 67 | alldata += data 68 | files[f"{cc}_{rectype}"] = data 69 | 70 | # Create a combined file as well 71 | files[f"{cc}_ALL"] = alldata 72 | 73 | # Write files to S3 74 | for filename, filedata in files.items(): 75 | 76 | if usedate: # Have a valid generation date for the data 77 | keydate = f"{yyyy}/{mm}/{dd}/{filename}" 78 | try: 79 | s3.Object(BUCKET, keydate).put(Body="\n".join(filedata)) 80 | fc += 1 81 | except Exception as e: 82 | logger.error(f"Failed to write s3:{BUCKET}/{keydate}") 83 | logger.exception(e) 84 | 85 | keylatest = f"latest/{filename}" 86 | try: 87 | s3.Object(BUCKET, keylatest).put(Body="\n".join(filedata)) 88 | fc += 1 89 | except Exception as e: 90 | logger.error(f"Failed to write s3:{BUCKET}/{keylatest}") 91 | logger.exception(e) 92 | return fc 93 | 94 | 95 | # Main lambda entry point, triggered by an EventBridge rule (parser event) 96 | def handler(event, context): 97 | # Try to set loglevel as defined by environment variable 98 | level = os.getenv("LogLevel", default="info").upper() 99 | if level: 100 | logger.setLevel(logging.getLevelName(level)) 101 | logger.debug(f"Log level set by environment variable: {level}") 102 | 103 | # Process event 104 | logger.debug(f"Received event: {event}") 105 | srcbucket = event["detail"]["bucket"]["name"] 106 | srckey = event["detail"]["object"]["key"] 107 | rir = os.path.basename(srckey).split("-")[1].upper() 108 | logger.debug(f"Processing {srckey} from {srcbucket}") 109 | 110 | # Read parsed file and struture the data 111 | ccdata = json.loads(read_s3_file(srcbucket, srckey)) 112 | fc = dbstore(ccdata) 113 | logger.info(f"Created {fc} files for {len(ccdata)} countries in {rir} region") 114 | 115 | return 116 | -------------------------------------------------------------------------------- /lambda/downloader.py: -------------------------------------------------------------------------------- 1 | # Download and verify a RIR delegation file as specified in the event URL 2 | 3 | import os 4 | import re 5 | import json 6 | import time 7 | import shutil 8 | import hashlib 9 | import logging 10 | from contextlib import closing 11 | from urllib.request import urlopen 12 | from urllib.parse import urlparse, unquote 13 | 14 | import boto3 15 | 16 | # AWS configuration 17 | REGION = "eu-west-1" # AWS region name 18 | BUCKET = "cc2asn-data" # S3 bucket name 19 | PREFIX = "RIR-SEF" # Folder in bucket 20 | 21 | # Setup logging 22 | logger = logging.getLogger(__name__) 23 | logger.setLevel(logging.DEBUG) 24 | 25 | # Download file from URL 26 | def download(url, tmpdir): 27 | fname = unquote(urlparse(url).path.split("/")[-1]) 28 | logger.debug(f"Downloading {url} to {tmpdir}/{fname}") 29 | try: 30 | with urlopen(url) as response: 31 | with open(f"{tmpdir}/{fname}", "wb") as f: 32 | shutil.copyfileobj(response, f) 33 | except Exception as e: 34 | logger.error(e) 35 | return None 36 | return f"{tmpdir}/{fname}" 37 | 38 | 39 | # Get origin MD5 sum of the delegation file 40 | def get_md5(url): 41 | try: 42 | with closing(urlopen(f"{url}.md5")) as r: 43 | md5sum = re.findall(r"[a-fA-F\d]{32}", r.read().decode("utf-8")).pop() 44 | logger.debug(f"Origin MD5: {md5sum}") 45 | return md5sum 46 | except IndexError: 47 | return None 48 | 49 | 50 | # Calculate MD5 of file 51 | def calc_md5(filename): 52 | md5 = hashlib.md5() 53 | with open(filename, "rb") as f: 54 | for chunk in iter(lambda: f.read(4096), b""): 55 | md5.update(chunk) 56 | logger.debug(f"Calculated MD5: {md5.hexdigest()}") 57 | return md5.hexdigest() 58 | 59 | 60 | # Save local temp file to S3 61 | def save_to_s3(tmpfile, bucket, key): 62 | try: 63 | logger.debug(f"Uploading {tmpfile} to S3://{bucket}/{key}") 64 | s3 = boto3.resource(service_name="s3", region_name=REGION) 65 | s3.Object(bucket, key).put(Body=open(tmpfile, "rb")) 66 | return f"S3://{bucket}/{key}" 67 | except Exception as e: 68 | logger.exception(e) 69 | return None 70 | 71 | 72 | # Create temporary space 73 | def mktmpdir(): 74 | tmpdir = "/tmp/cc2asn." + str(int(time.time())) 75 | try: 76 | os.mkdir(tmpdir) 77 | logger.debug(f"Created {tmpdir}") 78 | except FileExistsError: 79 | logger.warning(f"{tmpdir} already exists") 80 | pass 81 | return tmpdir 82 | 83 | 84 | # Remove all files from the temporary local storage 85 | def cleanup(tmpdir): 86 | try: 87 | shutil.rmtree(tmpdir) 88 | logger.debug(f"Cleaned up {tmpdir}") 89 | except Exception as e: 90 | logger.error(f"Failed to clean up {tmpdir}") 91 | logger.exception(e) 92 | 93 | 94 | # Main Lambda entry point 95 | def handler(event, context): 96 | # Set log level 97 | try: 98 | logger.setLevel(getattr(logging, event["loglevel"])) 99 | except (KeyError, AttributeError) as e: 100 | logger.setLevel(logging.INFO) 101 | logger.warning( 102 | f"No or invalid log level specified in event. Defaulting to INFO" 103 | ) 104 | 105 | # Process event 106 | logger.debug("Received event %s", event) 107 | if "url" not in event: 108 | logger.error("Missing url in input event!") 109 | else: 110 | tmpdir = mktmpdir() 111 | tmpfile = download(event["url"], tmpdir) 112 | if tmpfile is not None: 113 | logger.info("Successfully downloaded delegation file") 114 | if calc_md5(tmpfile) != get_md5(event["url"]): 115 | logger.error("Error! Invalid checksum") 116 | else: 117 | logger.info("Checksum OK") 118 | s3path = save_to_s3( 119 | tmpfile, BUCKET, f"{PREFIX}/{os.path.basename(tmpfile)}" 120 | ) 121 | if s3path is not None: 122 | logger.info(f"Successfully stored on {s3path}") 123 | else: 124 | logger.error("Error! Could not store file on S3") 125 | cleanup(tmpdir) 126 | else: 127 | logger.error("Failed to download delegation file") 128 | return 129 | -------------------------------------------------------------------------------- /lambda/parser.py: -------------------------------------------------------------------------------- 1 | # Parse a RIR delegation file according to the SEF specification 2 | # https://ftp.ripe.net/pub/stats/ripencc/RIR-Statistics-Exchange-Format.txt 3 | 4 | import os 5 | import json 6 | import math 7 | import logging 8 | import collections 9 | 10 | import boto3 11 | 12 | # AWS configuration 13 | REGION = "eu-west-1" # AWS region name 14 | PREFIX = "parsed" # Folder to store parsed files. Bucket is defined in event 15 | 16 | # Setup logging 17 | logger = logging.getLogger(__name__) 18 | 19 | # Function that reads a file from S3 and returns its content 20 | def read_s3_file(bucket, key): 21 | s3 = boto3.resource(service_name="s3", region_name=REGION) 22 | try: 23 | obj = s3.Object(bucket, key) 24 | data = obj.get()["Body"].read().decode("utf-8").splitlines() 25 | logger.info(f"Successfully read {bucket}/{key}") 26 | except Exception as e: 27 | logger.error(f"Failed to read {bucket}/{key}") 28 | logger.exception(e) 29 | data = None 30 | return data 31 | 32 | 33 | # Function that writes a file to S3 34 | def write_s3_file(bucket, key, data): 35 | s3 = boto3.resource(service_name="s3", region_name=REGION) 36 | try: 37 | obj = s3.Object(bucket, key) 38 | obj.put(Body=data) 39 | logger.info(f"Successfully wrote {bucket}/{key}") 40 | except Exception as e: 41 | logger.error(f"Failed to write {bucket}/{key}") 42 | logger.exception(e) 43 | return 44 | 45 | 46 | # SEF (Statistics Exchange Format) parser 47 | def parser(sefdata): 48 | logger.debug("Parsing SEF data") 49 | 50 | # Store parsed data in an ordered dictionary 51 | ccdata = collections.OrderedDict() 52 | 53 | # Data generation date (set by RIR) 54 | dgendate = None 55 | 56 | for ln in sefdata: 57 | 58 | # Remove all whitespace 59 | ln = ln.strip() 60 | 61 | # Skip all comments 62 | if ln.startswith("#"): 63 | continue 64 | 65 | # Skip summary 66 | if ln.endswith("summary"): 67 | continue 68 | 69 | # Skip non-allocated records 70 | if ln.rstrip("|").endswith("available") or ln.rstrip("|").endswith("reserved"): 71 | continue 72 | 73 | # Version line 74 | # 0 |1 |2 |3 |4 |5 |6 75 | # version|registry|serial|records|startdate|enddate|UTCoffset 76 | if ln[0].isdigit(): 77 | dgendate = ln.split("|")[5] 78 | continue 79 | 80 | # Extract records 81 | # 0 |1 |2 |3 |4 |5 |6     |7 82 | # registry|cc|type|start|value|date|status[|extensions...] 83 | elements = list(map(lambda x: x.upper(), ln.split("|"))) 84 | cc = elements[1] 85 | iptype = elements[2] 86 | start = str(elements[3]) 87 | value = int(elements[4]) 88 | 89 | # Process IP and ASNs records 90 | record = "" 91 | if iptype == "IPV4" or iptype == "IPV6": 92 | if ":" not in start: 93 | value = int(math.ceil(32 - math.log(value, 2))) 94 | record = start + "/" + str(value) 95 | elif iptype == "ASN": 96 | record = "AS" + start 97 | else: 98 | logger.warning(f"Undefined record type: {iptype}") 99 | 100 | # Structurize records 101 | typedata = {} 102 | if cc in ccdata: 103 | typedata = ccdata[cc] 104 | if iptype in typedata: 105 | data = typedata[iptype] 106 | data.append(record) 107 | typedata[iptype] = data 108 | ccdata[cc] = typedata 109 | else: 110 | typedata[iptype] = [record] 111 | ccdata[cc] = typedata 112 | else: 113 | typedata[iptype] = [record] 114 | ccdata[cc] = typedata 115 | logger.info(f"Parsed {len(sefdata)} SEF entries into {len(ccdata)} countries") 116 | 117 | # Add metadata 118 | ccdata.update({"DATE": dgendate}) # RIR Generation date 119 | 120 | return ccdata 121 | 122 | 123 | # Main lambda entry point, trigged by an EventBridge rule (new S3 obj created) 124 | def handler(event, context): 125 | # Try to set loglevel as defined by environment variable 126 | level = os.getenv("LogLevel", default="info").upper() 127 | if level: 128 | logger.setLevel(logging.getLevelName(level)) 129 | logger.debug(f"Log level set by environment variable: {level}") 130 | 131 | # Process event 132 | logger.debug(f"Received event: {event}") 133 | srcbucket = event["detail"]["bucket"]["name"] 134 | srckey = event["detail"]["object"]["key"] 135 | logger.debug(f"Processing {srckey} from {srcbucket}") 136 | 137 | # Parse and store SEF data 138 | ccdata = parser(read_s3_file(srcbucket, srckey)) 139 | parsedfile = f"{PREFIX}/{os.path.basename(srckey)}.json" 140 | write_s3_file(srcbucket, parsedfile, json.dumps(ccdata)) 141 | 142 | return 143 | -------------------------------------------------------------------------------- /lambda/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.26.59 2 | botocore==1.29.59 3 | jmespath==1.0.1 4 | natsort==8.2.0 5 | python-dateutil==2.8.2 6 | s3transfer==0.6.0 7 | six==1.16.0 8 | typing_extensions==4.4.0 9 | urllib3==1.26.14 10 | -------------------------------------------------------------------------------- /lambda/test_local_curator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import os 4 | import sys 5 | import curator 6 | import logging 7 | 8 | event = { 9 | "version": "0", 10 | "id": "ef2a17a3-5dc2-f7a9-8785-604f92e009c9", 11 | "detail-type": "Object Created", 12 | "source": "aws.s3", 13 | "account": "719411478741", 14 | "time": "2023-01-31T11:14:42Z", 15 | "region": "eu-west-1", 16 | "resources": ["arn:aws:s3:::cc2asn-data"], 17 | "detail": { 18 | "version": "0", 19 | "bucket": {"name": "cc2asn-data"}, 20 | "object": { 21 | "key": "parsed/delegated-afrinic-extended-latest.sef.json", 22 | "size": 2591859, 23 | "etag": "6a40279e655b97cb017947c708313423", 24 | "sequencer": "0063D8F820216A537E", 25 | }, 26 | "request-id": "X01QR7HBD7ZD0A17", 27 | "requester": "719411478741", 28 | "source-ip-address": "84.210.146.241", 29 | "reason": "PutObject", 30 | }, 31 | } 32 | context = "" 33 | os.environ["LogLevel"] = "DEBUG" 34 | 35 | handler = logging.StreamHandler(sys.stdout) 36 | curator.logger.addHandler(handler) 37 | curator.handler(event, context) 38 | -------------------------------------------------------------------------------- /lambda/test_local_downloader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import sys 4 | import downloader 5 | import logging 6 | 7 | # Download events 8 | # event = '{ "url": "https://ftp.ripe.net/ripe/stats/delegated-ripencc-extended-latest", "loglevel": "DEBUG" }' 9 | event = '{ "url": "https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest", "loglevel": "DEBUG" }' 10 | # event = '{ "url": "https://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-extended-latest", "loglevel": "DEBUG" }' 11 | # event = '{ "url": "https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest", "loglevel": "DEBUG" }' 12 | # event = '{ "url": "https://ftp.apnic.net/pub/stats/apnic/delegated-apnic-extended-latest", "loglevel": "DEBUG" }' 13 | context = "" 14 | 15 | handler = logging.StreamHandler(sys.stdout) 16 | downloader.logger.addHandler(handler) 17 | downloader.handler(json.loads(event), context) 18 | -------------------------------------------------------------------------------- /lambda/test_local_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import os 4 | import sys 5 | import parser 6 | import logging 7 | 8 | delegation_file = "delegated-ripencc-extended-latest" 9 | event = { 10 | "version": "0", 11 | "id": "193f49ac-d582-51d1-3aca-67c0bf54a745", 12 | "detail-type": "Object Created", 13 | "source": "aws.s3", 14 | "account": "719411478741", 15 | "time": "2023-01-30T14:04:33Z", 16 | "region": "eu-west-1", 17 | "resources": ["arn:aws:s3:::cc2asn-data"], 18 | "detail": { 19 | "version": "0", 20 | "bucket": {"name": "cc2asn-data"}, 21 | "object": { 22 | "key": f"{delegation_file}", 23 | "size": 1817, 24 | "etag": "8dece44db2d8aa29fa9367a17e327672", 25 | "version-id": "5G2q4qWw2ImSeAm8CucAil.7_iL6uFKc", 26 | "sequencer": "0063D7CE71A7D76012", 27 | }, 28 | "request-id": "ZR9RGS5N9YTHC6FN", 29 | "requester": "719411478741", 30 | "source-ip-address": "84.210.146.241", 31 | "reason": "PutObject", 32 | }, 33 | } 34 | context = "" 35 | os.environ["LogLevel"] = "DEBUG" 36 | 37 | handler = logging.StreamHandler(sys.stdout) 38 | parser.logger.addHandler(handler) 39 | parser.handler(event, context) 40 | -------------------------------------------------------------------------------- /legacy/README.md: -------------------------------------------------------------------------------- 1 | CC2ASN Legacy Server 2 | ==================== 3 | 4 | A simple lookup service for AS-numbers and prefixes belonging to any given country in the world. For more information check out the website at [www.cc2asn.com][1]. 5 | 6 | **Note:** if you're only interested in the CC2ASN data, I suggest you use the service on [www.cc2asn.com][1], either by querying the database or simply download the db.tar.gz package, rather than installing a completely new server. 7 | 8 | System prerequisites 9 | -------------------- 10 | 11 | sudo apt-get install python-naturalsort python-configobj rsyslog 12 | 13 | Installation 14 | ------------ 15 | 16 | curl -L https://github.com/toringe/cc2asn/tarball/master | tar zx 17 | cd *cc2asn* 18 | sudo ./install.sh 19 | 20 | Initializing with data 21 | ---------------------- 22 | 23 | sudo RIR-downloader.sh 24 | sudo SEF-parser.py 25 | 26 | You can run these scripts as a non-privileged user, provided that you set proper permissions on the `DATADIR` and `DBDIR` directories specified in the configuration file `cc2asn.conf` 27 | 28 | Starting the server 29 | ------------------- 30 | 31 | An Upstart-script is provided, and is installed by the installation script `install.sh`. This script will automatically start and stop the server at startup and shutdown. To start the server manually without doing a complete reboot, do the following: 32 | 33 | start cc2asn 34 | 35 | Checking the status of the server 36 | 37 | status cc2asn 38 | 39 | If you get `cc2asn start/running, process ` (where pid is the process id of the server on your system), then it's up and running. 40 | 41 | Troubleshooting 42 | --------------- 43 | 44 | Check your local syslog file for any problems with the CC2ASN service. Any log entries marked with `WARNING`, `ERROR` or `CRITICAL` should be dealt with. 45 | 46 | zgrep -iE "cc2asn: " /var/log/syslog* 47 | 48 | [1]: http://www.cc2asn.com 49 | 50 | -------------------------------------------------------------------------------- /legacy/RIR-downloader.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ################################################################################ 3 | # 4 | # RIR Downloader 5 | # --------------- 6 | # 7 | # Download each of the RIRs delegations as defined in the configuration file to 8 | # the defined data directory. There is no need to run this script more than once 9 | # a day, since the delegated files from the registrars are only updated daily. 10 | # 11 | # Exit codes: 12 | # 0 - OK 13 | # 1 - Failed to create DATADIR 14 | # 2 - Failed to write to DATADIR 15 | # 3 - Failed to download delegated file 16 | # 4 - Failed to download checksum 17 | # 5 - Failed to verify checksum 18 | # 19 | # Author: Tor Inge Skaar 20 | # 21 | ################################################################################ 22 | 23 | # Read configuration 24 | source "cc2asn.conf" 25 | 26 | # Create DATADIR if it doesn't exists 27 | if [ ! -d $DATADIR ]; then 28 | mkdir -p $DATADIR 29 | if [ $? != 0 ]; then 30 | exit 1 31 | fi 32 | fi 33 | if [ ! -w $DATADIR ]; then 34 | echo "Unable to write to $DATADIR" >&2 35 | exit 2 36 | fi 37 | curdir=`pwd` 38 | cd $DATADIR 39 | 40 | ecode=0 41 | i=1 42 | varname="RIR"$i 43 | while [ -n "${!varname}" ] 44 | do 45 | url=${!varname} 46 | rirfile=`basename $url` 47 | 48 | # Create header 49 | rir=`echo $rirfile | cut -d- -f2 | tr [:lower:] [:upper:]` 50 | head="--| $rir |--" 51 | headlen=${#head} 52 | twidth=`tput cols` 53 | echo -n $head; yes - | head -$(( $twidth - $headlen )) | paste -s -d '' - 54 | 55 | # Download delegated file 56 | echo "Downloading $url" 57 | wget -O $rirfile "$url" 58 | if [ $? != 0 ]; then 59 | echo "[ERROR] Wget failed to download $rirfile" >&2 60 | rm $rirfile 61 | ecode=3 62 | else 63 | if [ $MD5 ]; then 64 | # Download checksum 65 | url=$url".md5" 66 | md5file=$rirfile".md5" 67 | wget -O $md5file "$url" 68 | if [ $? != 0 ]; then 69 | echo "[ERROR] Wget failed to download $md5file" >&2 70 | rm $md5file 71 | ecode=4 72 | else 73 | # Latest file is only symlink, so md5 file contains actual name 74 | sed -i 's/-[0-9]\{8\}/-latest/' $md5file 75 | 76 | # Verify checksum 77 | md5sum -w -c $md5file 78 | if [ $? != 0 ]; then 79 | ecode=5 80 | fi 81 | fi 82 | fi 83 | fi 84 | echo 85 | let "i+=1" 86 | varname="RIR"$i 87 | done 88 | cd $curdir 89 | exit $ecode 90 | -------------------------------------------------------------------------------- /legacy/SEF-parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Parse RIR SEF files and output country spesific data files. 5 | Spec: ftp.ripe.net/pub/stats/ripencc/RIR-Statistics-Exchange-Format.txt 6 | 7 | Author: Tor Inge Skaar 8 | 9 | ''' 10 | # Core modules 11 | from __future__ import print_function 12 | import os 13 | import re 14 | import sys 15 | import math 16 | import configobj 17 | import collections 18 | 19 | # Third-party modules 20 | import natsort 21 | 22 | # Load configuration 23 | config = configobj.ConfigObj('cc2asn.conf') 24 | 25 | # Store parsed data in an ordered dictionary 26 | ccdata = collections.OrderedDict() 27 | 28 | # Parse each RIR SEF file 29 | for seffile in os.listdir(config.get('DATADIR')): 30 | if seffile.endswith("latest"): 31 | with open(os.path.join(config.get('DATADIR'), seffile)) as f: 32 | for line in f: 33 | 34 | # Remove all whitespace trash 35 | line = line.strip() 36 | 37 | # Skip all comments 38 | if line.startswith('#'): 39 | continue 40 | 41 | # Skip header 42 | if line[0].isdigit(): 43 | continue 44 | 45 | # Skip summary 46 | if line.endswith('summary'): 47 | continue 48 | 49 | # Skip not allocated records 50 | if (line.rstrip('|').endswith('available') or 51 | line.rstrip('|').endswith('reserved')): 52 | continue 53 | 54 | # Extract records 55 | # registry|cc|type|start|value|date|status[|extensions...] 56 | elements = map(lambda x: x.upper(), line.split('|')) 57 | cc = elements[1] 58 | iptype = elements[2] 59 | start = str(elements[3]) 60 | value = int(elements[4]) 61 | 62 | # Process prefixes and ASNs 63 | if iptype == 'IPV4' or iptype == 'IPV6': 64 | if ':' not in start: 65 | value = int(math.ceil(32 - math.log(value, 2))) 66 | record = start + '/' + str(value) 67 | elif iptype == 'ASN': 68 | record = 'AS' + start 69 | else: 70 | print("WARNING: Undefined record type: {}".format(iptype), 71 | file=sys.stderr) 72 | 73 | # Structurize records 74 | typedata = {} 75 | if cc in ccdata: 76 | typedata = ccdata[cc] 77 | if iptype in typedata: 78 | data = typedata[iptype] 79 | data.append(record) 80 | typedata[iptype] = data 81 | ccdata[cc] = typedata 82 | else: 83 | typedata[iptype] = [record] 84 | ccdata[cc] = typedata 85 | else: 86 | typedata[iptype] = [record] 87 | ccdata[cc] = typedata 88 | 89 | # Try to create directory for datafiles 90 | dbdir = config.get('DBDIR') 91 | if not os.path.exists(dbdir): 92 | try: 93 | os.makedirs(dbdir) 94 | except: 95 | print("ERROR: Unable to create directory: {}".format(dbdir), 96 | file=sys.stderr) 97 | exit(1) 98 | 99 | # Output data to country spesific files 100 | dbdir = config.get('DBDIR') 101 | for cc in ccdata: 102 | alldata = [] 103 | typedata = ccdata[cc] 104 | types = sorted(typedata.keys()) 105 | for iptype in types: 106 | filepath = os.path.join(dbdir, cc + '_' + iptype) 107 | if iptype == "IPV6": 108 | data = sorted(typedata[iptype]) 109 | else: 110 | data = natsort.natsort(typedata[iptype]) 111 | alldata += data 112 | 113 | with open(filepath, 'w+') as f: 114 | for item in data: 115 | print(item, file=f) 116 | 117 | # Create a combined file as well 118 | filepath = os.path.join(dbdir, cc + '_ALL') 119 | with open(filepath, 'w+') as f: 120 | for item in alldata: 121 | print(item, file=f) 122 | -------------------------------------------------------------------------------- /legacy/cc2asn-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | CC2ASN Server 5 | 6 | Basic query-response server listen on a tcp-port (default 43) for 7 | incoming requests. The only valid and required request is an 8 | ISO-3166-1 alpha-2 country code (e.g. NO for Norway). The server 9 | will respond back with the list of registered AS-numbers. 10 | Optionally a record type (IPv4, IPv6 or ALL) may be specified to 11 | get prefixes instead of ASNs, or to get everything that is 12 | registered for this country. Logs all system messages and client 13 | queries to local syslog. 14 | 15 | 16 | Author: Tor Inge Skaar 17 | 18 | ''' 19 | 20 | # Core modules 21 | import os 22 | import re 23 | import sys 24 | import pwd 25 | import grp 26 | import errno 27 | import signal 28 | import argparse 29 | import datetime 30 | import configobj 31 | import threading 32 | import SocketServer 33 | import logging 34 | from logging.handlers import SysLogHandler 35 | 36 | 37 | # Each time a client connect, a new instance of this class is created. 38 | class RequestHandler(SocketServer.BaseRequestHandler): 39 | 40 | # Handle the incomming request 41 | def handle(self): 42 | 43 | # Receive 8 bytes of data, and convert to uppercase 44 | try: 45 | sockdata = self.request.recv(8) 46 | except IOError as e: 47 | if e.errno == errno.ECONNRESET: 48 | self.server.logger.warning('Connection reset by client') 49 | return 50 | else: 51 | raise 52 | if sockdata is not None: 53 | sockdata = sockdata.strip().upper() 54 | else: 55 | self.server.logger.warning('No client data received') 56 | return 57 | 58 | # Client IP 59 | client = self.client_address[0] 60 | 61 | # First match cc search without rectype 62 | ccmatch = re.match('^([A-Z]{2})$', sockdata) 63 | if ccmatch is not None: 64 | # Defaulting to ASN 65 | rectype = 'ASN' 66 | cc = ccmatch.group(1) 67 | else: 68 | # Check if record type is defined 69 | recmatch = re.match('^(ALL|ASN|IPV4|IPV6) ([A-Z]{2})$', sockdata) 70 | if recmatch is not None: 71 | rectype = recmatch.group(1) 72 | cc = recmatch.group(2) 73 | else: 74 | self.server.logger.error('Invalid query from ' + client + 75 | ': ' + str(sockdata)) 76 | return 77 | 78 | # Construct path to file and send the contents to client 79 | datafile = cc + '_' + rectype 80 | datapath = os.path.join(self.server.config.get('DBDIR'), datafile) 81 | if os.path.isfile(datapath) and os.access(datapath, os.R_OK): 82 | with open(datapath, 'r') as data: 83 | self.request.send(data.read()) 84 | self.logclient(client, rectype, cc) 85 | else: 86 | self.server.logger.warning('Client ' + client + 87 | ' queried for missing file: '+datapath) 88 | return 89 | 90 | # Log client requests 91 | def logclient(self, ip, rectype, cc): 92 | if self.server.clientlog is None: 93 | # Use syslog 94 | self.server.logger.info('Query: ' + ip + ' ' + rectype + ' ' + cc) 95 | else: 96 | # Use custom log 97 | now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 98 | log = open(self.server.clientlog, 'a') 99 | log.write('{} {} {} {}\n'.format(now, ip, rectype, cc)) 100 | log.close() 101 | # End class 102 | 103 | 104 | # Change execution UID and GID 105 | def drop_privileges(uid_name, gid_name): 106 | 107 | # We're not root, so everythings fine then.. 108 | if os.getuid() != 0: 109 | return 110 | 111 | # Get the uid/gid from the name 112 | try: 113 | running_uid = pwd.getpwnam(uid_name).pw_uid 114 | except KeyError: 115 | e = 'Unable to drop privileges. No such user: {}'.format(uid_name) 116 | logger.critical(e) 117 | exit(e) 118 | try: 119 | running_gid = grp.getgrnam(gid_name).gr_gid 120 | except KeyError: 121 | e = 'Unable to drop privileges. No such group: {}'.format(gid_name) 122 | logger.critical(e) 123 | exit(e) 124 | 125 | # Remove group privileges 126 | os.setgroups([]) 127 | 128 | # Try setting the new uid/gid 129 | os.setgid(running_gid) 130 | os.setuid(running_uid) 131 | 132 | # Ensure a very conservative umask 133 | old_umask = os.umask(077) 134 | 135 | 136 | # Run process as a daemon by double forking 137 | def daemonize(): 138 | try: 139 | pid = os.fork() 140 | if pid > 0: 141 | # Exit first parent 142 | exit() 143 | except OSError as e: 144 | exit('Fork #1 failed: {} ({})'.format(e.errno, e.strerror)) 145 | 146 | os.chdir('/') 147 | os.setsid() 148 | os.umask(0) 149 | 150 | try: 151 | pid = os.fork() 152 | if pid > 0: 153 | # Exit second parent 154 | exit() 155 | except OSError as e: 156 | exit('Fork #2 failed: {} ({})'.format(e.errno, e.strerror)) 157 | 158 | 159 | # Handle user input 160 | def parse_input(): 161 | parser = argparse.ArgumentParser(description='CC2ASN Server') 162 | parser.add_argument('-c', dest='confpath', help='Path to config file') 163 | parser.add_argument('-d', dest='daemon', action='store_true', 164 | help='Daemonize server') 165 | parser.add_argument('-l', dest='clientlog', 166 | help='Log client requests to custom file') 167 | parser.add_argument('-p', dest='pidfile', help='Path to PID file') 168 | parser.add_argument('-V', action='version', version='CC2ASN Server v.1') 169 | args = parser.parse_known_args()[0] 170 | 171 | if args.confpath is None: 172 | args.confpath = '/etc/default/cc2asn' 173 | logger.info('No config file specified. Using {}'.format(args.confpath)) 174 | 175 | return args 176 | 177 | 178 | # Create an empty file 179 | def touch(filename, desc): 180 | if os.path.isfile(filename) is True: 181 | return 182 | else: 183 | try: 184 | f = open(filename, 'w+') 185 | f.close() 186 | logger.info('{}: {}'.format(desc, filename)) 187 | except IOError as e: 188 | errmsg = e.strerror + ': ' + filename 189 | logger.critical(errmsg) 190 | exit(errmsg) 191 | 192 | 193 | # Create subdirectory for pid file. 194 | # This enables deletion after we drop privileges. 195 | def create_piddir(piddir, user, group): 196 | 197 | # Create directory if needed 198 | if os.path.exists(piddir) is False: 199 | try: 200 | os.mkdir(piddir) 201 | except OSError as e: 202 | logger.error('Failed to create directory: {}'.format(piddir)) 203 | 204 | # Change owner 205 | try: 206 | uid = pwd.getpwnam(user).pw_uid 207 | gid = grp.getgrnam(group).gr_gid 208 | os.chown(piddir, uid, gid) 209 | except OSError as e: 210 | logger.error('Failed to chown {}'.format(piddir)) 211 | 212 | 213 | # Create PID file and check/set permissions 214 | def create_pidfile(pidfile, pid): 215 | 216 | if os.path.isfile(pidfile) is False: 217 | try: 218 | f = open(pidfile, 'w+') 219 | f.write(str(pid)) 220 | f.close() 221 | logger.info('PID file created: {}'.format(pidfile)) 222 | except IOError as e: 223 | logger.error('Failed to create pid file: {}'.format(pidfile)) 224 | else: 225 | logger.warning('PID file already exists. Stale file?') 226 | 227 | 228 | # Create signal handlers for the usual interrupts 229 | def signal_handling(): 230 | logger.info('Installing signal handlers') 231 | signal.signal(signal.SIGINT, cleanup) 232 | signal.signal(signal.SIGTERM, cleanup) 233 | signal.signal(signal.SIGQUIT, cleanup) 234 | 235 | 236 | # Cleanup process in separate thread 237 | def cleanup(signal, frame): 238 | logger.warning('Interrupted by {}'.format(signalname[signal])) 239 | t = threading.Thread(target=shutdown_handler, args=(shutdown_event,)) 240 | t.start() 241 | 242 | 243 | # Proper shutdown of socketserver 244 | def shutdown_handler(event): 245 | logger.info('Shutting down server') 246 | 247 | # Cleanly shutdown server 248 | try: 249 | server.shutdown() 250 | logger.info('Successful shutdown') 251 | except Exception as e: 252 | logger.error('Failed: {}'.format(e.strerror)) 253 | 254 | # Remove pid file 255 | try: 256 | # was config.get(pidfile) 257 | pidfile = args.pidfile 258 | logger.info('Removing PID file: {}'.format(pidfile)) 259 | os.remove(pidfile) 260 | except OSError as e: 261 | logger.warning('Failed to remove PID file. {}'.format(e.strerror)) 262 | 263 | # Tell thread that shutdown event is complete 264 | event.set() 265 | return 266 | 267 | 268 | # Main execution 269 | if __name__ == '__main__': 270 | 271 | # Log to local syslog 272 | logger = logging.getLogger('CC2ASN') 273 | logger.setLevel(logging.INFO) 274 | syslog = SysLogHandler(address='/dev/log') 275 | formatter = logging.Formatter('%(name)s: <%(levelname)s> - %(message)s') 276 | syslog.setFormatter(formatter) 277 | logger.addHandler(syslog) 278 | 279 | # Handle user input 280 | args = parse_input() 281 | 282 | # Create signal name lookup 283 | signalname = dict((k, v) for v, k in 284 | signal.__dict__.iteritems() if v.startswith('SIG')) 285 | signal_handling() 286 | 287 | # Load configuration 288 | if os.access(args.confpath, os.R_OK): 289 | config = configobj.ConfigObj(args.confpath) 290 | else: 291 | exit('Failed to read configuration file: {}'.format(args.confpath)) 292 | 293 | # Allow server to reuse a socket immediately after socket closure 294 | SocketServer.TCPServer.allow_reuse_address = True 295 | 296 | # Kill server thread when main thread terminates 297 | SocketServer.TCPServer.daemon_threads = True 298 | 299 | # Create a threaded TCP server, spawning separate threats for each client 300 | listen = int(config.get('PORT')) 301 | try: 302 | server = SocketServer.ThreadingTCPServer(('', listen), RequestHandler) 303 | (ip, port) = server.server_address 304 | logger.info('Server bound to {}:{}'.format(ip, port)) 305 | except IOError as e: 306 | if e.errno == 13: 307 | errmsg = 'Premission denied to bind port {}'.format(listen) 308 | else: 309 | errmsg = e.strerror 310 | logger.critical(errmsg) 311 | exit(errmsg) 312 | 313 | # Share variables with server 314 | server.clientlog = args.clientlog 315 | server.config = config 316 | server.logger = logger 317 | 318 | if args.daemon is True: 319 | 320 | # Get settings from config 321 | user = config.get('RUNUSER') 322 | group = config.get('RUNGROUP') 323 | 324 | # Set default pid file if not specified 325 | if args.pidfile is None: 326 | args.pidfile = '/var/run/cc2asn/cc2asn-server.pid' 327 | 328 | # Create subdirectory for pid file 329 | create_piddir(os.path.dirname(args.pidfile), user, group) 330 | 331 | # Drop root privileges 332 | drop_privileges(user, group) 333 | logger.info('Privileges dropped to {}:{}'.format(user, group)) 334 | 335 | # Daemonize 336 | daemonize() 337 | pid = os.getpid() 338 | logger.info('Daemonized (pid {})'.format(pid)) 339 | 340 | # Create PID file 341 | create_pidfile(args.pidfile, pid) 342 | 343 | else: 344 | logger.info('Server running in foreground (pid {})' 345 | .format(os.getpid())) 346 | 347 | # If custom log is set, create it if not exists 348 | if args.clientlog is not None: 349 | if os.path.isfile(args.clientlog) is False: 350 | touch(args.clientlog, 'Client log') 351 | else: 352 | if os.access(args.clientlog, os.W_OK) is False: 353 | errmsg = 'Unable to write to file: {}'.format(args.clientlog) 354 | logger.critical(errmsg) 355 | exit(errmsg) 356 | 357 | # Create an event for the shutdown process to set 358 | shutdown_event = threading.Event() 359 | 360 | # Server must handle requests indefinitely until a shutdown is requested 361 | server.serve_forever() 362 | 363 | # Main thread will wait for shutdown to finish 364 | shutdown_event.wait() 365 | -------------------------------------------------------------------------------- /legacy/cc2asn.conf: -------------------------------------------------------------------------------- 1 | # RIR sources 2 | RIR1="ftp://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest" 3 | RIR2="ftp://ftp.ripe.net/ripe/stats/delegated-ripencc-extended-latest" 4 | RIR3="ftp://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-extended-latest" 5 | RIR4="ftp://ftp.apnic.net/pub/stats/apnic/delegated-apnic-extended-latest" 6 | RIR5="ftp://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest" 7 | 8 | # Raw data directory 9 | DATADIR="/srv/cc2asn/data" 10 | 11 | # Database directory 12 | DBDIR="/srv/cc2asn/db" 13 | 14 | # Listen port 15 | PORT="43" 16 | 17 | # Run server as user/group 18 | RUNUSER="nobody" 19 | RUNGROUP="nogroup" 20 | -------------------------------------------------------------------------------- /legacy/cc2asn.upstart: -------------------------------------------------------------------------------- 1 | #!upstart 2 | 3 | description "Upstart script for the CC2ASN server" 4 | 5 | env PIDFILE="/var/run/cc2asn/cc2asn-server.pid" 6 | env CONFIG="/etc/default/cc2asn" 7 | env SERVER="/usr/local/sbin/cc2asn-server.py" 8 | 9 | start on (local-filesystems and net-device-up IFACE!=lo) 10 | stop on runlevel [!2345] 11 | 12 | respawn 13 | respawn limit 10 5 14 | 15 | expect daemon 16 | 17 | exec start-stop-daemon --start --pidfile $PIDFILE --exec $SERVER -- -c $CONFIG -p $PIDFILE -d 18 | -------------------------------------------------------------------------------- /legacy/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Install CC2ASN to local system 4 | 5 | if [ "$UID" -ne 0 ]; then 6 | echo "Need root privileges (e.g. sudo ./install.sh)" 7 | exit 8 | fi 9 | 10 | # Install CC2ASN configuration file 11 | DEST="/etc/default/cc2asn" 12 | if [ -e $DEST ]; then 13 | diff -q cc2asn.conf $DEST >/dev/null 14 | if [ $? != 0 ]; then 15 | printf "%`tput cols`s"|tr ' ' '-' 16 | echo "diff cc2asn.conf $DEST" 17 | diff cc2asn.conf $DEST 18 | printf "%`tput cols`s"|tr ' ' '-' 19 | echo "Installation script want to overwrite $DEST." 20 | echo "Any local customizations you have done will be lost." 21 | read -p "Are you sure you want to do this? [y/N]: " -n 1 -r 22 | echo 23 | if [[ $REPLY =~ ^[Yy]$ ]]; then 24 | cp cc2asn.conf $DEST 25 | fi 26 | fi 27 | else 28 | cp cc2asn.conf $DEST 29 | fi 30 | 31 | # Install Upstart configuration 32 | DEST="/etc/init/cc2asn.conf" 33 | if [ -e $DEST ]; then 34 | diff -q cc2asn.upstart $DEST >/dev/null 35 | if [ $? != 0 ]; then 36 | printf "%`tput cols`s"|tr ' ' '-' 37 | echo "diff cc2asn.upstart $DEST" 38 | diff cc2asn.upstart $DEST 39 | printf "%`tput cols`s"|tr ' ' '-' 40 | echo "Installation script want to overwrite $DEST." 41 | echo "Any local customizations you have done will be lost." 42 | read -p "Are you sure you want to do this? [y/N]: " -n 1 -r 43 | echo 44 | if [[ $REPLY =~ ^[Yy]$ ]]; then 45 | cp cc2asn.upstart /etc/init/cc2asn.conf 46 | initctl reload-configuration 47 | fi 48 | fi 49 | else 50 | cp cc2asn.upstart /etc/init/cc2asn.conf 51 | initctl reload-configuration 52 | fi 53 | 54 | # Install downloader and update config path 55 | cp RIR-downloader.sh /usr/local/sbin/ 56 | sed -i 's/cc2asn.conf/\/etc\/default\/cc2asn/' /usr/local/sbin/RIR-downloader.sh 57 | 58 | # Install parser and update config path 59 | cp SEF-parser.py /usr/local/sbin/ 60 | sed -i 's/cc2asn.conf/\/etc\/default\/cc2asn/' /usr/local/sbin/SEF-parser.py 61 | 62 | # Install server 63 | cp cc2asn-server.py /usr/local/sbin/ 64 | 65 | echo "Installation of CC2ASN is complete" 66 | 67 | -------------------------------------------------------------------------------- /misc/CC2ASN-Arch-Dark.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /misc/CC2ASN-Arch-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toringe/cc2asn/cee0e5f1e64789e5f489d3164a5668667fdece63/misc/CC2ASN-Arch-Dark.png -------------------------------------------------------------------------------- /misc/CC2ASN-Arch-Light.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /misc/CC2ASN-Arch-Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toringe/cc2asn/cee0e5f1e64789e5f489d3164a5668667fdece63/misc/CC2ASN-Arch-Light.png --------------------------------------------------------------------------------