armv7 build

This commit is contained in:
Fergal Moran
2020-11-14 21:38:13 +00:00
parent 04ffc26647
commit 7d242820cd
70 changed files with 970 additions and 183 deletions

View File

@@ -144,3 +144,4 @@ cython_debug/
app.db app.db
bitchmin.db bitchmin.db
.idea/ .idea/
app/dashboard.cfg

View File

@@ -1 +1 @@
BitchMin bitchmin-api

View File

@@ -0,0 +1,27 @@
FROM python:3.7-alpine
COPY ./requirements.txt /requirements.txt
RUN apk update && \
apk add --no-cache --virtual build-deps gcc python3-dev musl-dev bind-tools lapack libstdc++ && \
apk --no-cache add --virtual .builddeps g++ gcc gfortran musl-dev lapack-dev && \
apk add postgresql-dev && \
pip install scipy && apk del .builddeps && rm -rf /root/.cache && \
pip install -r requirements.txt
RUN pip install -r requirements.txt
RUN pip install gunicorn
COPY app /app
COPY migrations /migrations
COPY server.py boot.sh ./
RUN chmod +x boot.sh
ENV FLASK_ENV production
ENV FLASK_APP server.py
RUN chown -R bitchmin:bitchmin ./
USER bitchmin
EXPOSE 5000
ENTRYPOINT ["./boot.sh"]

View File

@@ -1,30 +1,22 @@
FROM python:3.7.4-alpine FROM python:3.7
RUN adduser -D bitchmin COPY ./requirements.txt /requirements.txt
RUN mkdir /bitchmin RUN apt update && \
WORKDIR /bitchmin apt install libblas-dev && \
COPY ./requirements.txt requirements.txt pip install gunicorn
RUN pip install -r requirements.txt
RUN apk update && \ COPY app /app
apk add --no-cache --virtual build-deps gcc python3-dev musl-dev bind-tools && \ COPY migrations /migrations
apk add postgresql-dev && \
pip install -r requirements.txt
RUN python3.7 -m venv venv
RUN venv/bin/pip install -r requirements.txt
RUN venv/bin/pip install gunicorn
COPY app app
COPY migrations migrations
COPY server.py boot.sh ./ COPY server.py boot.sh ./
RUN chmod +x boot.sh RUN chmod +x boot.sh
ENV FLASK_ENV production
ENV FLASK_APP server.py ENV FLASK_APP server.py
RUN chown -R bitchmin:bitchmin ./ #RUN chown -R bitchmin:bitchmin ./
USER bitchmin #USER bitchmin
EXPOSE 5000 EXPOSE 5000
ENTRYPOINT ["./boot.sh"] ENTRYPOINT ["./boot.sh"]

View File

@@ -9,8 +9,9 @@ from flask_jwt_extended import JWTManager
from flask_mail import Mail from flask_mail import Mail
from flask_migrate import Migrate from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
import flask_monitoringdashboard as dashboard
from app.config import Config from app.conf import load_config
from app.utils.encoders import IPAddressFieldEncoder from app.utils.encoders import IPAddressFieldEncoder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -18,19 +19,20 @@ logger = logging.getLogger(__name__)
db = SQLAlchemy() db = SQLAlchemy()
jwt = JWTManager() jwt = JWTManager()
mail = Mail() mail = Mail()
migrate = Migrate() migrate = Migrate()
CELERY_TASK_LIST = [ CELERY_TASK_LIST = [
'app.tasks.hosts', 'app.tasks.hosts',
] ]
import flask_monitoringdashboard as dashboard
def create_app(app_name='bitchmin', config_class=Config): def create_app(app_name='bitchmin'):
logger.info('Creating app {}'.format(app_name)) logger.info('Creating app {}'.format(app_name))
app = Flask(app_name) app = Flask(app_name)
app.config.from_object(config_class)
app.config.from_object(
load_config(os.environ.get('FLASK_ENV'))
)
logger.info('Creating database {}'.format(app.config['SQLALCHEMY_DATABASE_URI'])) logger.info('Creating database {}'.format(app.config['SQLALCHEMY_DATABASE_URI']))
db.init_app(app) db.init_app(app)
@@ -69,6 +71,8 @@ def create_app(app_name='bitchmin', config_class=Config):
app.logger.addHandler(file_handler) app.logger.addHandler(file_handler)
app.json_encoder = IPAddressFieldEncoder app.json_encoder = IPAddressFieldEncoder
dashboard.bind(app)
dashboard.config.init_from(envvar='FLASK_MONITORING_DASHBOARD_CONFIG')
db.init_app(app) db.init_app(app)
@@ -95,6 +99,3 @@ def create_celery_app(app=None):
celery.Task = ContextTask celery.Task = ContextTask
return celery return celery
celery = create_celery_app()

View File

@@ -1,41 +1,38 @@
import logging import logging
from datetime import timedelta from datetime import timedelta
from flask import jsonify, request from flask import jsonify, request, current_app
from flask_jwt_extended import ( from flask_jwt_extended import (
create_access_token, jwt_refresh_token_required, get_current_user
jwt_refresh_token_required, create_refresh_token,
get_current_user
) )
from sqlalchemy.orm.exc import NoResultFound
from app import db, jwt from app import db, jwt
from app.api import api from app.api import api
from app.config import Config
from app.models.user import User from app.models.user import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def __create_token(user_id):
expiry = timedelta(days=14) if Config.ISDEV else timedelta(minutes=15)
return create_access_token(
identity=user_id,
expires_delta=expiry,
fresh=True)
@jwt.user_loader_callback_loader @jwt.user_loader_callback_loader
def user_loader_callback(identity): def user_loader_callback(identity):
return User.by_id(identity) return db.session.query(User).get(identity)
@api.route('/auth/register/', methods=('POST',)) @api.route('/auth/register/', methods=('POST',))
def register(): def register():
data = request.get_json() data = request.get_json()
user = User(**data) user = User(**data)
db.session.add(user) try:
db.session.commit() db.session.query(User).filter_by(email=user.email).one()
return jsonify(user.to_dict()), 201 return jsonify({
'status': 'error',
'payload': 'User with email {} already exists.'.format(user.email)
}), 409
except NoResultFound:
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201
@api.route('/auth/login/', methods=('POST',)) @api.route('/auth/login/', methods=('POST',))
@@ -45,8 +42,8 @@ def login():
if not user: if not user:
return jsonify({'message': 'Invalid credentials', 'authenticated': False}), 401 return jsonify({'message': 'Invalid credentials', 'authenticated': False}), 401
access_token = __create_token(user.id) access_token = user.create_token(timedelta(days=14) if current_app.config['ISDEV'] else timedelta(minutes=15))
refresh_token = create_refresh_token(user.id) refresh_token = user.create_refresh_token()
return jsonify({ return jsonify({
'accessToken': access_token, 'accessToken': access_token,
@@ -61,8 +58,8 @@ def login():
@jwt_refresh_token_required @jwt_refresh_token_required
def token_refresh(): def token_refresh():
user = get_current_user() user = get_current_user()
access_token = __create_token(user.id) access_token = user.create_token()
refresh_token = create_refresh_token(user.id) refresh_token = user.create_refresh_token()
return jsonify({ return jsonify({
'accessToken': access_token, 'accessToken': access_token,

View File

@@ -1,3 +1,4 @@
import json
import logging import logging
import os import os
@@ -17,9 +18,9 @@ from app.utils.iputils import is_valid_ip, is_valid_hostname
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@api.route('/dns/hosts') @api.route('/dns/zones')
def get_hosts(): def get_zones():
return DnsZone.get_delete_put_post() return jsonify([zone.to_dict() for zone in db.session.query(DnsZone).all()]), 200
@api.route('/dns/refresh', methods=['POST']) @api.route('/dns/refresh', methods=['POST'])

View File

@@ -0,0 +1,18 @@
import os
def load_config(mode=os.environ.get('FLASK_ENV')):
"""Load config."""
try:
if mode == 'production':
from .production import ProductionConfig
return ProductionConfig
elif mode == 'testing':
from .testing import TestingConfig
return TestingConfig
else:
from .development import DevelopmentConfig
return DevelopmentConfig
except ImportError:
from .config import Config
return Config

View File

@@ -4,17 +4,14 @@ from datetime import timedelta
from dotenv import load_dotenv from dotenv import load_dotenv
basedir = os.path.abspath(os.path.dirname(__file__)) basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '../.env')) load_dotenv(os.path.join(basedir, '../../.env'))
ISDEV = os.getenv('FLASK_ENV') == 'development' ISDEV = os.getenv('FLASK_ENV') == 'development'
DEBUG_CONNECTION = 'postgresql+psycopg2://bitchmin:bitchmin@localhost/bitchmin'
# DEBUG_CONNECTION = 'sqlite:///' + os.path.join(basedir, '../app.db')
class Config(object): class Config(object):
ISDEV = ISDEV ISDEV = ISDEV
SECRET_KEY = os.getenv('SECRET_KEY') or 'you-will-never-guess' SECRET_KEY = os.getenv('SECRET_KEY') or 'you-will-never-guess'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or DEBUG_CONNECTION
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
LOG_TO_STDOUT = os.getenv('LOG_TO_STDOUT') LOG_TO_STDOUT = os.getenv('LOG_TO_STDOUT')
@@ -37,21 +34,3 @@ class Config(object):
'schedule': timedelta(minutes=15) 'schedule': timedelta(minutes=15)
} }
} }
class ProductionConfig(Config):
DEBUG = False
class StagingConfig(Config):
DEVELOPMENT = True
DEBUG = True
class DevelopmentConfig(Config):
DEVELOPMENT = True
DEBUG = True
class TestingConfig(Config):
TESTING = True

View File

@@ -0,0 +1,6 @@
from .config import Config
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://bitchmin:bitchmin@localhost/bitchmin'

View File

View File

@@ -0,0 +1,7 @@
import os
from .config import Config
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')

View File

@@ -0,0 +1,6 @@
from .config import Config
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/test.db'

View File

@@ -24,6 +24,12 @@ class DnsNameServer(db.Model, _BaseModelMixin, FlaskSerializeMixin):
self.host = host self.host = host
self.ip = ip self.ip = ip
def to_dict(self):
return {
'host': self.host,
'ip': self.ip
}
@dataclass @dataclass
class DnsZone(db.Model, _BaseModelMixin, FlaskSerializeMixin): class DnsZone(db.Model, _BaseModelMixin, FlaskSerializeMixin):
@@ -48,6 +54,19 @@ class DnsZone(db.Model, _BaseModelMixin, FlaskSerializeMixin):
return self._create_serial() return self._create_serial()
def to_dict(self):
ret_data = {
'name': self.zone_name,
'serial': self.serial,
'admin': self.admin,
'nameservers': [ns.to_dict() for ns in self.nameservers],
'hosts': [host.to_dict() for host in self.hosts]
}
return ret_data
# TODO: FUCK!!
admin = 'Ferg@lMoran.me'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
zone_name = db.Column(db.String(253), unique=True, nullable=False) zone_name = db.Column(db.String(253), unique=True, nullable=False)
serial = db.Column(db.Integer, default=_create_serial) serial = db.Column(db.Integer, default=_create_serial)
@@ -66,15 +85,24 @@ class DnsHost(db.Model, _BaseModelMixin, FlaskSerializeMixin):
host = db.Column(db.String(255), unique=True, nullable=False) host = db.Column(db.String(255), unique=True, nullable=False)
ip = db.Column(IPAddressType(255), nullable=False) ip = db.Column(IPAddressType(255), nullable=False)
type = db.Column(db.String(10), nullable=False)
ttl = db.Column(db.Integer(), nullable=False)
def __init__(self, zone, host, ip): def __init__(self, zone, host, ip, ttl=30, record_type='A'):
self.zone = zone self.zone = zone
self.host = host self.host = host
self.ip = ip self.ip = ip
self.ttl = ttl
self.type = record_type
pass pass
def to_dict(self): def to_dict(self):
return dict(id=self.id, host=self.host, ip=self.ip) return {
'name': self.host,
'type': self.type,
'ttl': self.ttl,
'ip': self.ip,
}
# @event.listens_for(DnsZone.hosts, 'append') # @event.listens_for(DnsZone.hosts, 'append')

View File

@@ -1,6 +1,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime, timedelta
from flask_jwt_extended import create_access_token, create_refresh_token
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
@@ -49,6 +50,15 @@ class User(db.Model, _BaseModelMixin):
return user return user
def create_refresh_token(self):
return create_refresh_token(self.id)
def create_token(self, expiry=timedelta(days=14)):
return create_access_token(
identity=self.id,
expires_delta=expiry,
fresh=True)
@classmethod @classmethod
def by_id(cls, user_id): def by_id(cls, user_id):
return cls.query.get(user_id) return cls.query.get(user_id)

View File

@@ -0,0 +1,32 @@
import pytest
from app import create_app
from app import db as _db
from seeder import DbSeeder
@pytest.fixture()
def app(request):
app = create_app()
ctx = app.app_context()
ctx.push()
def teardown():
ctx.pop()
request.addfinalizer(teardown)
return app
@pytest.fixture()
def db(app, request):
_db.app = app
seeder = DbSeeder(_db)
seeder.seed()
def teardown():
seeder.teardown()
request.addfinalizer(teardown)
return _db

View File

@@ -0,0 +1,12 @@
from app import create_app
from seeder import DbSeeder
if __name__ == '__main__':
app = create_app()
ctx = app.app_context()
ctx.push()
from app import db
db.app = app
DbSeeder(db).seed()

View File

@@ -1,19 +1,10 @@
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app import db
from app.models import DnsZone, User, DnsHost, DnsNameServer from app.models import DnsZone, User, DnsHost, DnsNameServer
class Scaffolder(object): class DbSeeder(object):
engine = create_engine(
'postgresql+psycopg2://bitchmin:bitchmin@localhost/bitchmin', def __init__(self, database):
poolclass=StaticPool) self._db = database
Session = sessionmaker(bind=engine)
session = Session()
def _create_user(self): def _create_user(self):
user = User( user = User(
@@ -21,11 +12,11 @@ class Scaffolder(object):
'Fergal Moran', 'Fergal Moran',
'topsecret' 'topsecret'
) )
self.session.add(user) self._db.session.add(user)
def _create_zone(self): def _create_zone(self):
zone = DnsZone('bitchmints.com') zone = DnsZone('bitchmints.com')
self.session.add(zone) self._db.session.add(zone)
return zone return zone
def _create_nameservers(self, zone): def _create_nameservers(self, zone):
@@ -36,7 +27,7 @@ class Scaffolder(object):
'10.1.1.10{}'.format(i) '10.1.1.10{}'.format(i)
) )
self.session.add(ns) self._db.session.add(ns)
def _create_hosts(self, zone): def _create_hosts(self, zone):
for i in range(1, 11): for i in range(1, 11):
@@ -45,24 +36,20 @@ class Scaffolder(object):
'host-{}'.format(i), 'host-{}'.format(i),
'10.1.1.{}'.format(i) '10.1.1.{}'.format(i)
) )
self.session.add(host) self._db.session.add(host)
def scaffold(self): def seed(self):
db.metadata.drop_all(self.engine) self._db.metadata.drop_all(self._db.engine)
db.metadata.create_all(self.engine) self._db.metadata.create_all(self._db.engine)
self._create_user() self._create_user()
z = self._create_zone() z = self._create_zone()
self._create_nameservers(z) self._create_nameservers(z)
self._create_hosts(z) self._create_hosts(z)
self.session.commit() self._db.session.commit()
self.session.flush() self._db.session.flush()
def teardown(self): def teardown(self):
db.metadata.drop_all(self.engine) self._db.metadata.drop_all(self._db.engine)
self.engine.dispose() self._db.engine.dispose()
if __name__ == '__main__':
Scaffolder().scaffold()

View File

@@ -0,0 +1,51 @@
from datetime import timedelta
from app.models import User
class TestAuthRoutes:
def test_no_duplicate_registration(self, db, app) -> None:
client = app.test_client()
request = {
'email': 'fergal.moran@gmail.com',
'full_name': 'Fergal Moran',
'password': 'topsecret'
}
rv = client.post(f"/auth/register/", json=request)
assert "409" in rv.status
def test_registration(self, db, app) -> None:
client = app.test_client()
request = {
'email': 'fergal.moran+testuser@gmail.com',
'full_name': 'Fergal Moran Test User',
'password': 'topsecret2'
}
rv = client.post(f"/auth/register/", json=request)
assert "201" in rv.status
def test_login_fails(self, db, app) -> None:
client = app.test_client()
request = {
'email': 'fergal.moran@gmail.com',
'password': 'asdasda'
}
rv = client.post(f"/auth/login/", json=request)
assert "401" in rv.status
def test_login_succeeds(self, db, app) -> None:
client = app.test_client()
request = {
'email': 'fergal.moran@gmail.com',
'password': 'topsecret'
}
rv = client.post(f"/auth/login/", json=request)
assert "200" in rv.status

View File

@@ -1,37 +1,22 @@
import uuid import uuid
import pytest
from sqlalchemy import func
from app.models import User, DnsZone, DnsNameServer, DnsHost from app.models import User, DnsZone, DnsNameServer, DnsHost
from scaffolder import Scaffolder
@pytest.fixture(scope="function") # or "module" (to teardown at a module level)
def db():
scaffolder = Scaffolder()
scaffolder.teardown()
scaffolder.scaffold()
yield scaffolder.session
scaffolder.teardown()
class TestDatabaseUpdate: class TestDatabaseUpdate:
def test_load(self, db) -> None: def test_load(self, db) -> None:
assert db.query(User).count() == 1 assert db.session.query(User).count() == 1
assert db.query(DnsZone).count() == 1 assert db.session.query(DnsZone).count() == 1
assert db.query(DnsNameServer).count() == 2 assert db.session.query(DnsNameServer).count() == 2
assert db.query(DnsHost).count() == 10 assert db.session.query(DnsHost).count() == 10
def test_serial_on_create(self, db) -> None: def test_serial_on_create(self, db) -> None:
zone = db.query(DnsZone).first() zone = db.session.query(DnsZone).first()
assert len(str(zone.serial)) == 10 assert len(str(zone.serial)) == 10
assert zone.get_serial_increment() == 10 assert zone.get_serial_increment() == 10
def test_serial_on_add_host(self, db) -> None: def test_serial_on_add_host(self, db) -> None:
zone = db.query(DnsZone).first() zone = db.session.query(DnsZone).first()
serial = zone.get_serial_increment() serial = zone.get_serial_increment()
DnsHost( DnsHost(
@@ -39,7 +24,7 @@ class TestDatabaseUpdate:
host=str(uuid.uuid4()), host=str(uuid.uuid4()),
ip='99.99.99.99' ip='99.99.99.99'
) )
db.commit() db.session.commit()
""" """
Probably better to test that the new serial is > old serial... Probably better to test that the new serial is > old serial...
Not hugely important that it's only ONE more than it Not hugely important that it's only ONE more than it
@@ -47,11 +32,10 @@ class TestDatabaseUpdate:
assert zone.get_serial_increment() > serial assert zone.get_serial_increment() > serial
def test_serial_on_update_host(self, db) -> None: def test_serial_on_update_host(self, db) -> None:
host = db.query(DnsHost).get(4) host = db.session.query(DnsHost).get(4)
serial = host.zone.get_serial_increment() serial = host.zone.get_serial_increment()
host.ip = '99.99.99.99' host.ip = '99.99.99.99'
db.commit() db.session.commit()
assert host.zone.get_serial_increment() > serial assert host.zone.get_serial_increment() > serial

View File

@@ -0,0 +1,20 @@
import json
from datetime import timedelta
from app.models import User
class TestUserRoutes:
def test_get_hosts(self, db, app) -> None:
client = app.test_client()
rv = client.get(f"/dns/zones")
assert "200" in rv.status
zones = json.loads(rv.data)
assert len(zones) == 1
assert zones[0]['name'] == 'bitchmints.com'
assert len(zones[0]['nameservers']) == 2
assert zones[0]['nameservers'][0]['host'] == 'host-1'
assert zones[0]['nameservers'][1]['host'] == 'host-2'
assert zones[0]['nameservers'][0]['ip'] == '10.1.1.101'
assert zones[0]['nameservers'][1]['ip'] == '10.1.1.102'

View File

@@ -0,0 +1,12 @@
from datetime import timedelta
from app.models import User
class TestUserRoutes:
def test_user_get(self, db, app) -> None:
client = app.test_client()
rv = client.get(f"/ping")
assert "200" in rv.status
assert "pong!" in str(rv.data)

View File

@@ -0,0 +1,14 @@
from datetime import timedelta
from app.models import User
class TestUserRoutes:
def test_user_get(self, db, app) -> None:
client = app.test_client()
user = db.session.query(User).first()
token = user.create_token(timedelta(seconds=60))
rv = client.get(f"/user", headers={"Authorization": "Bearer {}".format(token)})
assert "200" in rv.status

View File

@@ -1,4 +1,3 @@
#!/bin/sh #!/bin/sh
source venv/bin/activate
flask db upgrade flask db upgrade
exec gunicorn -b :5000 --access-logfile - --error-logfile - server:app exec gunicorn -b :5000 --access-logfile - --error-logfile - server:app

View File

@@ -2,5 +2,5 @@
docker buildx build \ docker buildx build \
--push \ --push \
--platform linux/arm/v7,linux/amd64 \ --platform linux/arm/v7 \
-t fergalmoran/bitchmin-api . -t fergalmoran/bitchmin-api .

Binary file not shown.

View File

@@ -24,8 +24,8 @@ logger = logging.getLogger('alembic.env')
from flask import current_app from flask import current_app
config.set_main_option( config.set_main_option(
'sqlalchemy.url', 'sqlalchemy.url',
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%')) str(current_app.extensions['migrate']._db.engine.url).replace('%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata target_metadata = current_app.extensions['migrate']._db.metadata
# other values from the config, defined by the needs of env.py, # other values from the config, defined by the needs of env.py,
# can be acquired: # can be acquired:

View File

@@ -1,6 +0,0 @@
ls
rm -rf migrations || \
rm bitchmin/bitchmin.db || \
flask db init || \
flask db migrate -m "Initial" || \
flask db upgrade

View File

@@ -20,7 +20,9 @@ sqlalchemy
requests requests
IPy IPy
pydig pydig
tinder
twilio twilio
flask_monitoringdashboard flask_monitoringdashboard
pytest jinja2==2.11.1
pytest
pytest-flask
pytest-flask-sqlalchemy

View File

@@ -1,7 +1,9 @@
from flask_migrate import Migrate, upgrade from flask_migrate import Migrate, upgrade
from app import create_app, db from app import create_app, db, create_celery_app
app = create_app() app = create_app()
celery = create_celery_app()
migrate = Migrate(app, db) migrate = Migrate(app, db)

View File

@@ -1 +0,0 @@
BitchMin

View File

@@ -0,0 +1,17 @@
FROM python:3.7-alpine
RUN apk add --update alpine-sdk
COPY requirements.txt /
RUN pip install -r /requirements.txt
COPY . /app
WORKDIR /app
ENV DNS_PORT 53
ENV WORKER_PORT 10054
EXPOSE 53
EXPOSE 10054
CMD [ "python", "./server.py" ]

View File

View File

@@ -0,0 +1,69 @@
import json
from types import SimpleNamespace
class Zone(object):
def __init__(self, serial, name, admin, hosts):
self._serial = serial
self._name = name
self._admin = admin
self._hosts = hosts
@staticmethod
def from_json(data):
try:
zones = json.loads(data, object_hook=lambda d: SimpleNamespace(**d))
return [Zone(
x.serial,
x.name,
x.admin,
{h.name: Host(
h.ip,
h.name,
h.ttl,
h.type
) for h in x.hosts}
) for x in zones]
except Exception as e:
print(e)
@property
def name(self):
return self._name
@property
def admin(self):
return self._admin
@property
def serial(self):
return self._serial
@property
def hosts(self):
return self._hosts
class Host(object):
def __init__(self, ip, name, ttl, record_type):
self._ip = ip
self._name = name
self._ttl = ttl
self._record_type = record_type
@property
def ip(self):
return self._ip
@property
def name(self):
return self._name
@property
def ttl(self):
return self._ttl
@property
def record_type(self):
return self._record_type

View File

@@ -1 +1,4 @@
twisted twisted
pytest
dnspython
requests

View File

@@ -1,3 +1,4 @@
import requests
from twisted.internet import defer from twisted.internet import defer
from twisted.names import dns, error from twisted.names import dns, error
import logging import logging
@@ -8,7 +9,8 @@ class InvalidZoneException(Exception):
class Resolver: class Resolver:
def _build_host(self, record_type, host, ip, ttl): @staticmethod
def _build_host(record_type, host, ip, ttl):
return { return {
host: { host: {
'type': record_type, 'type': record_type,
@@ -68,7 +70,7 @@ class MemoryResolver(Resolver):
""" """
try: try:
host = self._parse_host(zone, str(query.name)) host = self._parse_host(zone, str(query.name))
record = self._zones[zone]['hosts'][host] record = self._zones[zone].hosts[host]
if query.type == dns.NS: if query.type == dns.NS:
payload = dns.Record_NS( payload = dns.Record_NS(
@@ -77,14 +79,14 @@ class MemoryResolver(Resolver):
) )
else: else:
payload = dns.Record_A( payload = dns.Record_A(
address=record['ip'], address=record.ip,
ttl=record['ttl'] ttl=record.ttl
) )
answer = dns.RRHeader( answer = dns.RRHeader(
name=zone, name=zone,
payload=payload, payload=payload,
ttl=record['ttl'] ttl=record.ttl
) )
answers = [answer] answers = [answer]

View File

@@ -1,56 +1,40 @@
import json
import logging import logging
import os
import requests
from twisted.internet import reactor from twisted.internet import reactor
from twisted.internet.endpoints import TCP4ServerEndpoint from twisted.internet.endpoints import TCP4ServerEndpoint
from twisted.names import client, dns, server from twisted.names import client, dns, server
from models.zone import Zone
from resolvers.memory_resolver import MemoryResolver from resolvers.memory_resolver import MemoryResolver
from servers.worker_server import WorkerServerFactory from servers.worker_server import WorkerServerFactory
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
PORT = 10053 PORT = os.environ.get('DNS_PORT') or 10053
WORKER_PORT = 10054 WORKER_PORT = os.environ.get('WORKER_PORT') or 10054
API_HOST = os.environ.get('API_HOST') or 'http://localhost:5000/dns/zones'
# TODO: This smells
def get_zones():
response = requests.get(API_HOST)
zones = Zone.from_json(response.text)
return {
zone.name: zone
for zone in zones}
def main(): def main():
zones = { zones = get_zones()
'bitchmints.com': {
'serial': 'BOOO',
'admin': 'Ferg@lMoran.me',
'nameservers': [
'ns1.bitchmints.com',
'ns2.bitchmints.com'
],
'hosts': {
'ns1': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.7'},
'ns2': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.8'},
'host-1': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.1'},
'host-2': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.1'},
'host-3': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.1'},
'host-4': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.1'},
}
},
'fergl.ie': {
'serial': 'BOOO',
'admin': 'Ferg@lMoran.me',
'nameservers': [
'ns1.bitchmints.com',
'ns2.bitchmints.com'
],
'hosts': {
'farts': {'type': 'A', 'ttl': 30, 'ip': '10.1.33.8'},
}
}
}
memory_resolver = MemoryResolver(zones) memory_resolver = MemoryResolver(zones)
dns_factory = server.DNSServerFactory( dns_factory = server.DNSServerFactory(
clients=[ clients=[
memory_resolver, memory_resolver,
client.Resolver(resolv='/etc/resolv.conf')] client.Resolver(resolv='/etc/resolv.conf')]
) )
protocol = dns.DNSDatagramProtocol(controller=dns_factory) protocol = dns.DNSDatagramProtocol(controller=dns_factory)

View File

View File

@@ -0,0 +1,14 @@
import pytest
@pytest.fixture()
def resolver():
import dns.resolver
r = dns.resolver.Resolver()
r.nameservers = ['127.0.0.1']
r.nameserver_ports = {
'127.0.0.1': 10053
}
return r

View File

@@ -0,0 +1,4 @@
def test_zones_exist(resolver) -> None:
answer = resolver.resolve('host-1.bitchmints.com')
assert answer.address == '10.1.1.1'

View File

@@ -0,0 +1,153 @@
###############################
# Core EditorConfig Options #
###############################
root = true
# All files
[*]
indent_style = space
indent_size = 4
# Code files
[*.{cs,csx,vb,vbx}]
indent_size = 4
insert_final_newline = true
charset = utf-8-bom
###############################
# .NET Coding Conventions #
###############################
[*.{cs,vb}]
# Organize usings
dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false
# this. preferences
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_event = false:silent
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
dotnet_style_readonly_field = true:suggestion
# Expression-level preferences
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
###############################
# Naming Conventions #
###############################
# Style Definitions
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Use PascalCase for constant fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
dotnet_naming_rule.private_members_with_underscore.severity = suggestion
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.prefix_underscore.capitalization = camel_case
dotnet_naming_style.prefix_underscore.required_prefix = _
###############################
# C# Code Style Rules #
###############################
[*.cs]
# var preferences
csharp_style_var_for_built_in_types = true:silent
csharp_style_var_when_type_is_apparent = true:silent
csharp_style_var_elsewhere = true:silent
# Expression-bodied members
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
# Pattern-matching preferences
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
# Null-checking preferences
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
# Expression-level preferences
csharp_prefer_braces = true:silent
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
###############################
# C# Formatting Rules #
###############################
# New line preferences
csharp_new_line_before_open_brace = false
csharp_new_line_before_else = false
csharp_new_line_before_catch = false
csharp_new_line_before_finally = false
csharp_new_line_before_members_in_object_initializers = false
csharp_new_line_before_members_in_anonymous_types = false
csharp_new_line_between_query_expression_clauses = false
# Indentation preferences
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
# Space preferences
csharp_space_after_cast = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_around_binary_operators = before_and_after
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true

View File

@@ -0,0 +1,27 @@
using System;
using System.Linq;
using System.Net;
using DnsClient;
using DnsClient.Protocol;
Console.WriteLine("Testing lookup");
var endpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 10053);
var client = new LookupClient(endpoint);
for (int i = 1; i <= 10; i++) {
var host = $"host-{i}.bitchmints.com";
Console.WriteLine($"Performing lookup for {host}");
var results = client
.Query(host, QueryType.A)
.Answers
.ARecords();
if (results.Any()) {
foreach (var aRecord in results) {
Console.WriteLine($"\tDomain: {aRecord.DomainName} resolved to {aRecord.Address}");
}
} else {
Console.WriteLine($"\tDomain: {host} was not found");
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,41 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v5.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v5.0": {
"dns-test/1.0.0": {
"dependencies": {
"DnsClient": "1.3.2"
},
"runtime": {
"dns-test.dll": {}
}
},
"DnsClient/1.3.2": {
"runtime": {
"lib/netstandard2.1/DnsClient.dll": {
"assemblyVersion": "1.3.2.0",
"fileVersion": "1.3.2.0"
}
}
}
}
},
"libraries": {
"dns-test/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"DnsClient/1.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-+3pku+O8VzuAirIezOhOa0rz0bK5SDtDgYUzVDbKxzFj9/zsMjvUAqS6kkxnUyX4d3L5d5MRR7gF2wLudODBYg==",
"path": "dnsclient/1.3.2",
"hashPath": "dnsclient.1.3.2.nupkg.sha512"
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
{
"runtimeOptions": {
"additionalProbingPaths": [
"/home/fergalm/.dotnet/store/|arch|/|tfm|",
"/home/fergalm/.nuget/packages"
]
}
}

View File

@@ -0,0 +1,9 @@
{
"runtimeOptions": {
"tfm": "net5.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "5.0.0"
}
}
}

Binary file not shown.

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>dns_test</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DnsClient" Version="1.3.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v5.0", FrameworkDisplayName = "")]

Binary file not shown.

View File

@@ -0,0 +1,23 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("dns-test")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("dns-test")]
[assembly: System.Reflection.AssemblyTitleAttribute("dns-test")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Generated by the MSBuild WriteCodeFragment class.

View File

@@ -0,0 +1 @@
9f361857c6c60ffd4891f6a8e0304ff6fe4b537e

View File

@@ -0,0 +1,8 @@
is_global = true
build_property.TargetFramework = net5.0
build_property.TargetPlatformMinVersion =
build_property.UsingMicrosoftNETSdkWeb =
build_property.ProjectTypeGuids =
build_property.PublishSingleFile =
build_property.IncludeAllContentForSelfExtract =
build_property._SupportedPlatformList = Android,iOS,Linux,macOS,Windows

View File

@@ -0,0 +1 @@
70cfeda8d1e9e2eb58e13e423a234b2e09b3c59b

View File

@@ -0,0 +1,18 @@
/home/fergalm/dev/BitchMin/working/dns-test/bin/Debug/net5.0/dns-test
/home/fergalm/dev/BitchMin/working/dns-test/bin/Debug/net5.0/dns-test.deps.json
/home/fergalm/dev/BitchMin/working/dns-test/bin/Debug/net5.0/dns-test.runtimeconfig.json
/home/fergalm/dev/BitchMin/working/dns-test/bin/Debug/net5.0/dns-test.runtimeconfig.dev.json
/home/fergalm/dev/BitchMin/working/dns-test/bin/Debug/net5.0/dns-test.dll
/home/fergalm/dev/BitchMin/working/dns-test/bin/Debug/net5.0/ref/dns-test.dll
/home/fergalm/dev/BitchMin/working/dns-test/bin/Debug/net5.0/dns-test.pdb
/home/fergalm/dev/BitchMin/working/dns-test/obj/Debug/net5.0/dns-test.csprojAssemblyReference.cache
/home/fergalm/dev/BitchMin/working/dns-test/obj/Debug/net5.0/dns-test.GeneratedMSBuildEditorConfig.editorconfig
/home/fergalm/dev/BitchMin/working/dns-test/obj/Debug/net5.0/dns-test.AssemblyInfoInputs.cache
/home/fergalm/dev/BitchMin/working/dns-test/obj/Debug/net5.0/dns-test.AssemblyInfo.cs
/home/fergalm/dev/BitchMin/working/dns-test/obj/Debug/net5.0/dns-test.csproj.CoreCompileInputs.cache
/home/fergalm/dev/BitchMin/working/dns-test/obj/Debug/net5.0/dns-test.dll
/home/fergalm/dev/BitchMin/working/dns-test/obj/Debug/net5.0/ref/dns-test.dll
/home/fergalm/dev/BitchMin/working/dns-test/obj/Debug/net5.0/dns-test.pdb
/home/fergalm/dev/BitchMin/working/dns-test/obj/Debug/net5.0/dns-test.genruntimeconfig.cache
/home/fergalm/dev/BitchMin/working/dns-test/bin/Debug/net5.0/DnsClient.dll
/home/fergalm/dev/BitchMin/working/dns-test/obj/Debug/net5.0/dns-test.csproj.CopyComplete

Binary file not shown.

View File

@@ -0,0 +1 @@
1e5a2e58d8a5d8f0bca0aaa4ee624df8facbe5c3

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,66 @@
{
"format": 1,
"restore": {
"/home/fergalm/dev/BitchMin/working/dns-test/dns-test.csproj": {}
},
"projects": {
"/home/fergalm/dev/BitchMin/working/dns-test/dns-test.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "/home/fergalm/dev/BitchMin/working/dns-test/dns-test.csproj",
"projectName": "dns-test",
"projectPath": "/home/fergalm/dev/BitchMin/working/dns-test/dns-test.csproj",
"packagesPath": "/home/fergalm/.nuget/packages/",
"outputPath": "/home/fergalm/dev/BitchMin/working/dns-test/obj/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/home/fergalm/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"net5.0"
],
"sources": {
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net5.0": {
"targetAlias": "net5.0",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
}
},
"frameworks": {
"net5.0": {
"targetAlias": "net5.0",
"dependencies": {
"DnsClient": {
"target": "Package",
"version": "[1.3.2, )"
}
},
"imports": [
"net461",
"net462",
"net47",
"net471",
"net472",
"net48"
],
"assetTargetFallback": true,
"warn": true,
"frameworkReferences": {
"Microsoft.NETCore.App": {
"privateAssets": "all"
}
},
"runtimeIdentifierGraphPath": "/home/fergalm/dotnet/sdk/5.0.100/RuntimeIdentifierGraph.json"
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/fergalm/.nuget/packages/</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/fergalm/.nuget/packages/</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">5.8.0</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="$([MSBuild]::EnsureTrailingSlash($(NuGetPackageFolders)))" />
</ItemGroup>
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,106 @@
{
"version": 3,
"targets": {
"net5.0": {
"DnsClient/1.3.2": {
"type": "package",
"compile": {
"lib/netstandard2.1/DnsClient.dll": {}
},
"runtime": {
"lib/netstandard2.1/DnsClient.dll": {}
}
}
}
},
"libraries": {
"DnsClient/1.3.2": {
"sha512": "+3pku+O8VzuAirIezOhOa0rz0bK5SDtDgYUzVDbKxzFj9/zsMjvUAqS6kkxnUyX4d3L5d5MRR7gF2wLudODBYg==",
"type": "package",
"path": "dnsclient/1.3.2",
"files": [
".nupkg.metadata",
".signature.p7s",
"dnsclient.1.3.2.nupkg.sha512",
"dnsclient.nuspec",
"icon.png",
"lib/net45/DnsClient.dll",
"lib/net45/DnsClient.xml",
"lib/net471/DnsClient.dll",
"lib/net471/DnsClient.xml",
"lib/netstandard1.3/DnsClient.dll",
"lib/netstandard1.3/DnsClient.xml",
"lib/netstandard2.0/DnsClient.dll",
"lib/netstandard2.0/DnsClient.xml",
"lib/netstandard2.1/DnsClient.dll",
"lib/netstandard2.1/DnsClient.xml"
]
}
},
"projectFileDependencyGroups": {
"net5.0": [
"DnsClient >= 1.3.2"
]
},
"packageFolders": {
"/home/fergalm/.nuget/packages/": {}
},
"project": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "/home/fergalm/dev/BitchMin/working/dns-test/dns-test.csproj",
"projectName": "dns-test",
"projectPath": "/home/fergalm/dev/BitchMin/working/dns-test/dns-test.csproj",
"packagesPath": "/home/fergalm/.nuget/packages/",
"outputPath": "/home/fergalm/dev/BitchMin/working/dns-test/obj/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/home/fergalm/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"net5.0"
],
"sources": {
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net5.0": {
"targetAlias": "net5.0",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
}
},
"frameworks": {
"net5.0": {
"targetAlias": "net5.0",
"dependencies": {
"DnsClient": {
"target": "Package",
"version": "[1.3.2, )"
}
},
"imports": [
"net461",
"net462",
"net47",
"net471",
"net472",
"net48"
],
"assetTargetFallback": true,
"warn": true,
"frameworkReferences": {
"Microsoft.NETCore.App": {
"privateAssets": "all"
}
},
"runtimeIdentifierGraphPath": "/home/fergalm/dotnet/sdk/5.0.100/RuntimeIdentifierGraph.json"
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"version": 2,
"dgSpecHash": "eyFn1mF0Ec0RODelM//pl87ne40rmd2/fm30g+WxWQ97TpqhUsZ79YryHC9MGUYH+wDk/epuUARQ5ofTUycO6w==",
"success": true,
"projectFilePath": "/home/fergalm/dev/BitchMin/working/dns-test/dns-test.csproj",
"expectedPackageFiles": [
"/home/fergalm/.nuget/packages/dnsclient/1.3.2/dnsclient.1.3.2.nupkg.sha512"
],
"logs": []
}