Commit ae9cfe5c authored by Edvard Rejthar's avatar Edvard Rejthar

flask server running

parent 7870ca11
# DOESNT WORK – process can know its run in docker
FROM ubuntu:16.04
#RUN apt update && apt install -y \
python3 \
python3-pip \
xvfb \
firefox \
# && pip3 install \
xvfbwrapper \
pymysql
RUN ./INSTALL
#RUN apt-get -y update && apt-get install -y fortunes
#CMD /usr/games/fortune -a | cowsay
CMD i="0";while [ $i -lt 4 ]; do echo "1";sleep 1; done
#CMD firefox
\ No newline at end of file
......@@ -10,7 +10,7 @@ apt install software-properties-common
add-apt-repository "deb http://archive.ubuntu.com/ubuntu $(lsb_release -sc) main universe restricted multiverse"
apt update
apt install firefox python3 mariadb-server xvfb
pip3 install xvfbwrapper pymysql peewee jinja2 pyyaml bs4 pygments pillow requests
pip3 install xvfbwrapper pymysql peewee flask wtforms pyyaml bs4 pygments pillow requests
# current dir
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
......@@ -30,7 +30,7 @@ cp -r .mozilla $DESTINATION
cp *.md $DESTINATION
cd $DESTINATION
# copy firefox profiles (about:config is stored at prefs.js file)
# copy firefox profiles (about:config is stored at pref.js file)
for(( i=1; i<=$((PROFILE_COUNT-1)); i++ ))
do
DEST=$DESTINATION/.mozilla/firefox/$i/
......
......@@ -6,7 +6,7 @@ Scans a website for a sign of a parasite hosts or commands.
1. Download ```git clone git@gitlab.labs.nic.cz:csirt/mdmaug.git /tmp/mdmaug```
2. Edit mdmaug/lib/config.py
3. You should generate certificate `openssl req -new -x509 -keyout cert-mdmaug.pem -out cert-mdmaug.pem -days 365 -nodes` to `mdmaug/cert-mdmaug.pem`
3. You should generate a certificate to `mdmaug/cert-mdmaug.pem`, at least a self-signed one (non recommended): `openssl req -new -x509 -keyout cert-mdmaug.pem -out cert-mdmaug.pem -days 365 -nodes`
4. Perform installation: ```/tmp/mdmaug/INSTALL```
5. Everything should be located in `/opt/mdmaug`.
6. Launch under newly created `mdmaug` user: `su - mdmaug -c 'python3 -m mdmaug'`
......@@ -25,7 +25,7 @@ Scans a website for a sign of a parasite hosts or commands.
## What is done to Firefox profiles?
We want no block nor safebrowsing warning. If you created the profiles manually, you'd use ```firefox -P```, the profiles names being: 0,1...
For about:config changes, see prefs.js. IE:
For about:config changes, see pref.js. IE:
* toolkit.startup.max_resumed_crashes = -1 (protoze i kdyz prohlizec nekdy killnu, nesmi me pri spusteni otravovat gui popupem)
* network.http.accept-encoding = "" # ukladame streamy, ale neumim je rozzipovat
* extensions.autoDisableScopes = "0" # moznost instalovat ze vsech umisteni
......
#!/usr/bin/env python3
import logging
import os
import ssl
import threading
logging.basicConfig(level=logging.DEBUG, format="%(message)s")
from http.server import HTTPServer
from flask import Flask, request
from xvfbwrapper import Xvfb
# import ipdb; ipdb.set_trace()
from .lib.config import Config
from .lib.controller.server import Server
from .lib.controller.api import Api
# import ipdb; ipdb.set_trace()
logging.basicConfig(level=logging.DEBUG, format="%(message)s")
# assure the logging dir
if not os.path.exists(Config.LOG_DIR):
os.makedirs(Config.LOG_DIR)
# server setup
Api.reset()
address = '0.0.0.0'
# address = '0.0.0.0'
app = Flask(__name__, static_url_path="/static")
app.secret_key = b'as8!r"afERaa5'
app.config["preferences"] = {
"safebrowsing": True,
"pdns": True,
"geoip": False
}
"""
httpd = HTTPServer((address, Config.APP_PORT), Server)
httpd.socket = ssl.wrap_socket(httpd.socket,
server_side=True,
# together private + cert, http://stackoverflow.com/questions/19705785/python-3-https-webserver
certfile=Config.DIR + 'cert-mdmaug.pem',
ssl_version=ssl.PROTOCOL_TLSv1)
"""
display = Xvfb()
display.start()
try:
print(f'Listening at https://{address}:{Config.APP_PORT}')
for _ in range(Config.profileCount):
threading.Thread(target=httpd.serve_forever).start()
# print(f'Listening at https://{address}:{Config.APP_PORT}')
from .lib.controller.server import app as server
app.register_blueprint(server)
app.run(host='0.0.0.0', ssl_context=(Config.DIR + 'cert-mdmaug.pem', Config.DIR + 'key-mdmaug.pem'), threaded=True)
# for _ in range(Config.profileCount):
# threading.Thread(target=httpd.serve_forever).start()
except (KeyboardInterrupt, SystemExit):
display.stop()
'''
XX TO BE DELETED:
How to debug mysql:
conn = pymysql.connect(host='localhost', user='root', passwd='lopuch', db='mdmaug', charset='utf8')
cur = conn.cursor()
cur.execute("""SELECT name from turris JOIN status ON status.id = turris.status WHERE ip = %s""", (ip,))
vote = self.cur.fetchone()[0]
logging.debug(vote)
exit()
from lib.model.dbp import Status, Export, Turris, Whitelist
url = "http://stat.cz"
logging.debug(Whitelist.select().count())
quit()
#ip = "112.78.117.153"
#logging.debug(Turris.select().join(Status, on=(Status.id == Turris.status)).where(Turris.ip == ip).limit(1).get().id)
#status = Turris.select().join(Status, on=(Status.id == Turris.status)).where(Turris.ip == ip).limit(1).get().status
#logging.debug(status)
#Turris.select().where(Turris.id == 5).count()
#from urllib.parse import parse_qs
#from urllib.parse import urlparse
#quit()
'''
......@@ -2,6 +2,7 @@ import logging
import os
import threading
from flask import g
from peewee import MySQLDatabase
......
......@@ -25,18 +25,16 @@ class ScanController:
profile = "-1" # bookovany profile firefoxu
queueFF = {}
# cacheDir = None
# logDir = None
def get_domain_snapdirs(self, domain, full_dirs=True):
dir = Config.CACHE_DIR + domain + "/"
if os.path.isdir(dir):
return [str(dir + subdir) if full_dirs else str(subdir) for subdir in os.listdir(dir) # adresare vsech moznych snapshotu
if os.path.isdir(str(dir + subdir)) and os.path.isfile(dir + subdir + "/" + ScanController.CRAWL_FILE)]
##
# @param cached Pokud chceme zobrazit cachovanou verzi analyzy, dejme True. Pokud dame int, je to maximalni stari (ve dnech). Kdyz se nenalezne, zanazyzuje se znovu.
def launch(self, url, cached=None):
"""
:type cached: True = Any cached version, int = cached version X days old. If None or not found, site will be reanalysed
"""
if cached:
snapdirs = self.get_domain_snapdirs(Domains.domain2dir(url))
if snapdirs:
......
import json
import logging
import mimetypes
import os
from http.server import SimpleHTTPRequestHandler
from collections import defaultdict
from jinja2 import Environment
from jinja2 import FileSystemLoader
from flask import Blueprint, send_from_directory, render_template, request, make_response, current_app
from jinja2 import Environment, FileSystemLoader
from wtforms import Form
from wtforms.fields import BooleanField
from ..config import Config
from ..controller.api import Api
......@@ -15,95 +15,104 @@ from ..model.dbp import Export
env = Environment()
env.loader = FileSystemLoader(Config.DIR + "templates/")
app = Blueprint('app', __name__, template_folder='templates')
class Server(SimpleHTTPRequestHandler):
def favicon(self):
with open('favicon.ico', 'rb') as f:
self.output(f.read(), "image/x-icon")
def render_template(self, filename, **kwargs):
self.output(env.get_template(filename).render(kwargs))
def output(self, contents, content_type="text/html"):
self.send_response(200)
self.send_header("Content-type", content_type)
self.end_headers()
try:
self.wfile.write(contents)
except:
self.wfile.write(contents.encode("UTF-8"))
def homepage(self):
self.render_template("homepage.html")
def static_file(self, url):
# 'rb' if is a binary string, else 'r'
type_ = 'rb' if bool(open('/usr/bin/python', 'rb').read(1024).translate(None,
bytearray([7, 8, 9, 10, 12, 13, 27]) + bytearray(
range(0x20, 0x100)))) else 'r'
with open(url, type_) as f:
self.output(f.read(), content_type=mimetypes.guess_type(url)[0])
def do_GET(self):
"""
Routing table:
/ → homepage
/existing-file → return the static file from /static
/(destination=example.com/)api... → if set, the output will be HTML5-postMessaged to other tab at the destination (with https protocol)
/api(=json)/ → output might be either in JSON, or else in HTML
/api/analyze(=...)/URI
/api/vote/...
/api/reset
/export/(days) → CSV of last X days
"""
_, path = self.path.split("/", 1)
path, *_ = path.split("?", 1)
if path == "":
return self.homepage()
elif os.path.isfile(Config.DIR + "static/" + path): # favicon or any other existing file
return self.static_file(Config.DIR + "static/" + path)
DbModel.assureConnection()
# parse the request url into a friendly dictionary
request = {"page": ""}
page = False
for l in self.path.split("/")[1:]:
if not page:
c, *d = l.split("=", 1)
if c in ["http:", "https:"]:
def update_preferences():
""" cookies → config """
for k, v in request.cookies.items():
if v is "0":
v = False
elif v is "1":
v = True
current_app.config["preferences"][k] = v
@app.route('/test')
def index():
resp = make_response("fds")
resp.set_cookie('safebrowsing', b"0")
resp.set_cookie('ahoj', b"1")
return resp
@app.route('/')
def homepage():
update_preferences()
class OptionsForm(Form):
pref = defaultdict(bool, current_app.config["preferences"])
safebrowsing = BooleanField('Google Safebrowsing', default=pref["safebrowsing"])
pdns = BooleanField('PDNS', default=pref["pdns"])
geoip = BooleanField('geoip', default=pref["geoip"])
if request.method == 'POST':
name = request.form['name']
return 'Hello ' + name
form = OptionsForm()
return render_template("homepage.html", form=form)
@app.route('/favicon.ico')
def favicon():
return send_from_directory('static', 'favicon.ico', mimetype='image/x-icon')
@app.route("/<path:request_url>")
def controller(request_url):
"""
Routing table:
/(destination=example.com/)api... → if set, the output will be HTML5-postMessaged to other tab at the destination (with https protocol)
/api(=json)/ → output might be either in JSON, or else in HTML
/api/analyze(=...)/URI
/api/vote/...
/api/reset
/export/(days) → CSV of last X days
"""
path, *_ = request_url.split("?", 1)
DbModel.assureConnection()
# parse the request url into a friendly dictionary
request = {"page": ""}
page = False
for l in request_url.split("/"):
if not page:
c, *d = l.split("=", 1)
if c in ["http:", "https:"]:
page = True
else:
request[c] = d[0] if len(d) else True
if c == "nicify":
l = l[6:]
page = True
else:
request[c] = d[0] if len(d) else True
if c == "nicify":
l = l[6:]
page = True
else:
continue
request["page"] += l + "/"
if request["page"]: # strip last slash
request["page"] = request["page"][:-1]
logging.debug("Request: {}".format(request))
if "api" in request: # /api/analyze/web
output = Api().run(request)
if request["api"] == "json":
if type(output) is dict:
output = json.dumps(output)
elif type(output) in [str, list]:
output = json.dumps(output)
if "destination" in request:
# send everything up, we are in an iframe
self.render_template("_message.html", contents=output, url=self.path,
destination=f"https://{request['destination']}/")
else:
self.output(output)
elif "export" in request: # /export/{days} - csv of last 7 days
url = self.path.split("/", 2)
self.output(Export.export_view(days=url[2]))
continue
request["page"] += l + "/"
if request["page"]: # strip last slash
request["page"] = request["page"][:-1]
logging.debug("Request: {}".format(request))
if "api" in request: # /api/analyze/web
output = Api().run(request)
if request["api"] == "json":
if type(output) is dict:
output = json.dumps(output)
elif type(output) in [str, list]:
output = json.dumps(output)
if "destination" in request:
# send everything up, we are in an iframe
render_template("_message.html", contents=output, url=request_url, destination=f"https://{request['destination']}/")
else:
return output
elif "export" in request: # /export/{days} - csv of last 7 days
url = request_url.split("/", 1)
return Export.export_view(days=url[2])
else:
return "MDMaug: Page not found", 404
......@@ -78,13 +78,14 @@ class Domains:
"""
return None, None # #23 service down
##
# Kontaktuje sluzbu safebrowsing a snazi se z jejich nekonzistentnich udaju vycist, zda kdyz na URL clovek pristoupi, objevi se cervena stranka.
# To zavisi na vicero udajich - napr. na tom, kdy si vas prohlizec updatoval seznam oproti sluzbe safebrowsing.
# Taky je mozne, ze sluzba zmenila wording. Mnoho zdaru!
#
# @param format 'bool' Vraci bool True/False/None, nebo 'attr' vraci int "1"/"0"/"" pro atribut
@staticmethod
def is_suspicious(domain, output='bool'):
"""
Scrape Safebrowsing service webpages and try to read out if the site is considered dangerous to visit.
:param domain:
:param output: 'bool' → True/False/None or 'attr' → int "1", "0", "" for an HTML attribute
:return:
"""
# contents = urllib.request.urlopen('http://www.google.com/safebrowsing/diagnostic?site=' + domain).read().decode("utf-8")
# with open("debugsf.tmp","a") as f:
# f.write(contents + "\n\n")
......
auxiliary.org-netbeans-modules-css-prep.less_2e_compiler_2e_options=
auxiliary.org-netbeans-modules-css-prep.less_2e_configured=true
auxiliary.org-netbeans-modules-css-prep.less_2e_enabled=true
auxiliary.org-netbeans-modules-css-prep.less_2e_mappings=/static:/static
auxiliary.org-netbeans-modules-css-prep.sass_2e_compiler_2e_options=
auxiliary.org-netbeans-modules-css-prep.sass_2e_enabled=false
auxiliary.org-netbeans-modules-css-prep.sass_2e_mappings=/scss:/css
file.reference.mdmaug-installer-mdmaug=.
files.encoding=UTF-8
site.root.folder=${file.reference.mdmaug-installer-mdmaug}
source.folder=${file.reference.mdmaug-installer-mdmaug}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://www.netbeans.org/ns/project/1">
<type>org.netbeans.modules.web.clientproject</type>
<configuration>
<data xmlns="http://www.netbeans.org/ns/clientside-project/1">
<name>mdmaug</name>
</data>
</configuration>
</project>
'''
XX TO BE DELETED:
How to debug mysql:
conn = pymysql.connect(host='localhost', user='root', passwd='', db='mdmaug', charset='utf8')
cur = conn.cursor()
cur.execute("""SELECT name from turris JOIN status ON status.id = turris.status WHERE ip = %s""", (ip,))
vote = self.cur.fetchone()[0]
logging.debug(vote)
exit()
from lib.model.dbp import Status, Export, Turris, Whitelist
url = "http://stat.cz"
logging.debug(Whitelist.select().count())
quit()
#ip = "112.78.117.153"
#logging.debug(Turris.select().join(Status, on=(Status.id == Turris.status)).where(Turris.ip == ip).limit(1).get().id)
#status = Turris.select().join(Status, on=(Status.id == Turris.status)).where(Turris.ip == ip).limit(1).get().status
#logging.debug(status)
#Turris.select().where(Turris.id == 5).count()
#from urllib.parse import parse_qs
#from urllib.parse import urlparse
#quit()
'''
{"version":3,"sources":["./analysis-style.less"],"names":[],"mappings":"AAAA,aAAc;EACV,YAAA;;AAGJ,sBACI;EACI,WAAA;;AAIR,SACI;AAD0B,sBAC1B;EACI,YAAA;;AAFR,SAII;AAJ0B,sBAI1B;EACI,YAAA;EACA,iBAAA;;AANR,SAQI;AAR0B,sBAQ1B;EACI,cAAA;;AATR,SAQI,WAEI;AAVsB,sBAQ1B,WAEI;EACI,WAAA;EACA,gBAAA;;AAZZ,SAQI,WAMI;AAdsB,sBAQ1B,WAMI;EACI,iBAAA;;AAfZ,SAkBI;AAlB0B,sBAkB1B;EACI,gBAAA;;AAnBR,SAkBI,MAEI;AApBsB,sBAkB1B,MAEI;EAEI,YAAA;EACA,SAAA;;AACA,SANR,MAEI,GAIK;AAAD,sBANR,MAEI,GAIK;EACG,WAAA;;AAzBhB,SA6BI;AA7B0B,sBA6B1B;EACI,iBAAA;;AA9BR,SAgCI,KAAI,iBACA;AAjCsB,sBAgC1B,KAAI,iBACA;AAjCR,SAgCI,KAAI,iBACO;AAjCe,sBAgC1B,KAAI,iBACO;EACH,aAAA;;AAlCZ,SAqCI,KAAI;AArCsB,sBAqC1B,KAAI;EACA,sBAAA;;AAtCR,SAqCI,KAAI,iBAEA,MACA;AAxCsB,sBAqC1B,KAAI,iBAEA,MACA;EACI,YAAA;;AAzCZ,SA6CI,KAAI;AA7CsB,sBA6C1B,KAAI;EACA,sBAAA;;AAGR;EACI,WAAA;;AAEJ;EACI,qBAAA;EACA,WAAA;EACA,uBAAA;EACA,YAAA","file":"analysis-style.css"}
\ No newline at end of file
{"version":3,"sources":["analysis-style.less"],"names":[],"mappings":"AAAA,aAAc;EACV,YAAA;;AAGJ,sBACI;EACI,WAAA;;AAIR,SACI;AAD0B,sBAC1B;EACI,YAAA;;AAFR,SAII;AAJ0B,sBAI1B;EACI,YAAA;EACA,iBAAA;;AANR,SAQI;AAR0B,sBAQ1B;EACI,cAAA;;AATR,SAQI,WAEI;AAVsB,sBAQ1B,WAEI;EACI,WAAA;EACA,gBAAA;;AAZZ,SAQI,WAMI;AAdsB,sBAQ1B,WAMI;EACI,iBAAA;;AAfZ,SAkBI;AAlB0B,sBAkB1B;EACI,gBAAA;;AAnBR,SAkBI,MAEI;AApBsB,sBAkB1B,MAEI;EAEI,YAAA;EACA,SAAA;;AACA,SANR,MAEI,GAIK;AAAD,sBANR,MAEI,GAIK;EACG,WAAA;;AAzBhB,SA6BI;AA7B0B,sBA6B1B;EACI,iBAAA;;AA9BR,SAgCI,KAAI,iBACA;AAjCsB,sBAgC1B,KAAI,iBACA;AAjCR,SAgCI,KAAI,iBACO;AAjCe,sBAgC1B,KAAI,iBACO;EACH,aAAA;;AAlCZ,SAqCI,KAAI;AArCsB,sBAqC1B,KAAI;EACA,sBAAA;;AAtCR,SAqCI,KAAI,iBAEA,MACA;AAxCsB,sBAqC1B,KAAI,iBAEA,MACA;EACI,YAAA;;AAzCZ,SA6CI,KAAI;AA7CsB,sBA6C1B,KAAI;EACA,sBAAA;;AAGR;EACI,WAAA;;AAEJ;EACI,qBAAA;EACA,WAAA;EACA,uBAAA;EACA,YAAA","file":"analysis-style.css"}
\ No newline at end of file
......@@ -23,7 +23,7 @@ $("input[type=url]").val(new URL(window.location.href).searchParams.get("url"));
/**
* list scans button
$("#list-scans").click(() => {
let urls = get_urls();
if (urls) {
......@@ -81,7 +81,7 @@ $("#request-url-toggler").change(function () {
* @param {type} rest_url Part of the URI of the API.
* @returns {undefined}
*/
function launch_request(rest_url, method = "json", header = "Result", callback=null) {
function launch_request(rest_url, method="json", header="Result", callback=null) {
let counter = 0;
//let uri = "/api={0}/analyze={1}/{2}".format(method, cache, url);
let uri = "/api={0}{1}".format(method, rest_url);
......
.switch {
font-size: 1rem;
position: relative; }
.switch input {
position: absolute;
height: 1px;
width: 1px;
background: none;
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
overflow: hidden;
padding: 0; }
.switch input + label {
position: relative;
min-width: calc(calc(2.375rem * .8) * 2);
border-radius: calc(2.375rem * .8);
height: calc(2.375rem * .8);
line-height: calc(2.375rem * .8);
display: inline-block;
cursor: pointer;
outline: none;
user-select: none;
vertical-align: middle;
text-indent: calc(calc(calc(2.375rem * .8) * 2) + .5rem); }
.switch input + label::before,
.switch input + label::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: calc(calc(2.375rem * .8) * 2);
bottom: 0;
display: block; }
.switch input + label::before {
right: 0;
background-color: #dee2e6;
border-radius: calc(2.375rem * .8);
transition: 0.2s all; }
.switch input + label::after {
top: 2px;
left: 2px;
width: calc(calc(2.375rem * .8) - calc(2px * 2));
height: calc(calc(2.375rem * .8) - calc(2px * 2));
border-radius: 50%;
background-color: white;
transition: 0.2s all; }
.switch input:checked + label::before {
background-color: #08d; }
.switch input:checked + label::after {
margin-left: calc(2.375rem * .8); }
.switch input:focus + label::before {
outline: none;
box-shadow: 0 0 0 0.2rem rgba(0, 136, 221, 0.25); }
.switch input:disabled + label {
color: #868e96;
cursor: not-allowed; }
.switch input:disabled + label::before {
background-color: #e9ecef; }
.switch.switch-sm {
font-size: 0.875rem; }
.switch.switch-sm input + label {
min-width: calc(calc(1.9375rem * .8) * 2);
height: calc(1.9375rem * .8);
line-height: calc(1.9375rem * .8);
text-indent: calc(calc(calc(1.9375rem * .8) * 2) + .5rem); }
.switch.switch-sm input + label::before {
width: calc(calc(1.9375rem * .8) * 2); }
.switch.switch-sm input + label::after {
width: calc(calc(1.9375rem * .8) - calc(2px * 2));
height: calc(calc(1.9375rem * .8) - calc(2px * 2)); }
.switch.switch-sm input:checked + label::after {
margin-left: calc(1.9375rem * .8); }
.switch.switch-lg {
font-size: 1.25rem; }
.switch.switch-lg input + label {
min-width: calc(calc(3rem * .8) * 2);
height: calc(3rem * .8);
line-height: calc(3rem * .8);
text-indent: calc(calc(calc(3rem * .8) * 2) + .5rem); }
.switch.switch-lg input + label::before {
width: calc(calc(3rem * .8) * 2); }
.switch.switch-lg input + label::after {
width: calc(calc(3rem * .8) - calc(2px * 2));
height: calc(calc(3rem * .8) - calc(2px * 2)); }
.switch.switch-lg input:checked + label::after {
margin-left: calc(3rem * .8); }
.switch + .switch {
margin-left: 1rem; }
/*# sourceMappingURL=switch-button.css.map */
{
"version": 3,
"mappings": "AA2CA,OAAQ;EACN,SAAS,EAjCM,IAAI;EAkCnB,QAAQ,EAAE,QAAQ;EAElB,aAAM;IACJ,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,GAAG;IACX,KAAK,EAAE,GAAG;IACV,UAAU,EAAE,IAAI;IAChB,MAAM,EAAE,CAAC;IACT,IAAI,EAAE,aAAa;IACnB,SAAS,EAAE,UAAU;IACrB,QAAQ,EAAE,MAAM;IAChB,OAAO,EAAE,CAAC;IAEV,qBAAQ;MACN,QAAQ,EAAE,QAAQ;MAClB,SAAS,EAAE,6BAA2B;MACtC,aAAa,EA7BI,mBAAc;MA8B/B,MAAM,EA9BW,mBAAc;MA+B/B,WAAW,EA/BM,mBAAc;MAgC/B,OAAO,EAAE,YAAY;MACrB,MAAM,EAAE,OAAO;MACf,OAAO,EAAE,IAAI;MACb,WAAW,EAAE,IAAI;MACjB,cAAc,EAAE,MAAM;MACtB,WAAW,EAAE,2CAAyC;IAGxD;gCACe;MACb,OAAO,EAAE,EAAE;MACX,QAAQ,EAAE,QAAQ;MAClB,GAAG,EAAE,CAAC;MACN,IAAI,EAAE,CAAC;MACP,KAAK,EAAE,6BAA2B;MAClC,MAAM,EAAE,CAAC;MACT,OAAO,EAAE,KAAK;IAGhB,6BAAgB;MACd,KAAK,EAAE,CAAC;MACR,gBAAgB,EApDV,OAA4B;MAqDlC,aAAa,EAtDI,mBAAc;MAuD/B,UAAU,EA9CI,QAAQ;IAiDxB,4BAAe;MACb,GAAG,EApDc,GAAG;MAqDpB,IAAI,EArDa,GAAG;MAsDpB,KAAK,EAAE,yCAA4D;MACnE,MAAM,EAAE,yCAA4D;MACpE,aAAa,EAzDU,GAAG;MA0D1B,gBAAgB,EA3DJ,KAAM;MA4DlB,UAAU,EAxDI,QAAQ;IA2DxB,qCAA0B;MACxB,gBAAgB,EAnEF,IAAiC;IAsEjD,oCAAyB;MACvB,WAAW,EAzEM,mBAAc;IA4EjC,mCAAwB;MACtB,OAAO,EAAE,IAAI;MACb,UAAU,EAtEU,oCAAyE;IAyE/F,8BAAmB;MACjB,KAAK,EA9Ea,OAA0C;MA+E5D,MAAM,EAAE,WAAW;IAGrB,sCAA2B;MACzB,gBAAgB,EApFD,OAAqC;EAyFxD,iBAAY;IACV,SAAS,EAhHG,QAAsB;IAmHhC,+BAAQ;MACN,SAAS,EAAE,8BAA8B;MACzC,MAAM,EApGK,oBAA+B;MAqG1C,WAAW,EArGA,oBAA+B;MAsG1C,WAAW,EAAE,4CAA4C;IAG3D,uCAAgB;MACd,KAAK,EAAE,8BAA8B;IAGvC,sCAAe;MACb,KAAK,EAAE,0CAA+D;MACtE,MAAM,EAAE,0CAA+D;IAGzE,8CAAyB;MACvB,WAAW,EAnHA,oBAA+B;EAyHhD,iBAAY;IACV,SAAS,EA5IG,OAAsB;IA+IhC,+BAAQ;MACN,SAAS,EAAE,yBAA8B;MACzC,MAAM,EA9HK,eAA+B;MA+H1C,WAAW,EA/HA,eAA+B;MAgI1C,WAAW,EAAE,uCAA4C;IAG3D,uCAAgB;MACd,KAAK,EAAE,yBAA8B;IAGvC,sCAAe;MACb,KAAK,EAAE,qCAA+D;MACtE,MAAM,EAAE,qCAA+D;IAGzE,8CAAyB;MACvB,WAAW,EA7IA,eAA+B;EAkJhD,iBAAU;IACR,WAAW,EAAE,IAAI",
"sources": ["switch-button.scss"],
"names": [],
"file": "switch-button.css"
}
\ No newline at end of file
//
// Switches for Bootstrap 4.
//
// - Fully customizable with Sass variables
// - No JavaScript required
// - Fully accessible
//
//
// IMPORTANT: These Sass variables are defined in Bootstrap's variables.scss. You should import that file first, then remove these.
//
$font-size-base: 1rem;
$font-size-lg: ($font-size-base * 1.25);
$font-size-sm: ($font-size-base * .875);
$input-height: 2.375rem;
$input-height-sm: 1.9375rem;
$input-height-lg: 3rem;
$input-btn-focus-width: .2rem;
$custom-control-indicator-bg: #dee2e6;
$custom-control-indicator-disabled-bg: #e9ecef;
$custom-control-description-disabled-color: #868e96;
$white: white;
$theme-colors: (
'primary': #08d
);
//
// These variables can be used to customize the switch component.
//
$switch-height: calc(#{$input-height} * .8) !default;
$switch-height-sm: calc(#{$input-height-sm} * .8) !default;
$switch-height-lg: calc(#{$input-height-lg} * .8) !default;
$switch-border-radius: $switch-height !default;
$switch-bg: $custom-control-indicator-bg !default;
$switch-checked-bg: map-get($theme-colors, 'primary') !default;
$switch-disabled-bg: $custom-control-indicator-disabled-bg !default;
$switch-disabled-color: $custom-control-description-disabled-color !default;
$switch-thumb-bg: $white !default;
$switch-thumb-border-radius: 50% !default;
$switch-thumb-padding: 2px !default;
$switch-focus-box-shadow: 0 0 0 $input-btn-focus-width rgba(map-get($theme-colors, 'primary'), .25);
$switch-transition: .2s all !default;
.switch {
font-size: $font-size-base;
position: relative;
input {
position: absolute;
height: 1px;
width: 1px;
background: none;
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
overflow: hidden;
padding: 0;
+ label {
position: relative;
min-width: calc(#{$switch-height} * 2);
border-radius: $switch-border-radius;
height: $switch-height;
line-height: $switch-height;
display: inline-block;
cursor: pointer;
outline: none;
user-select: none;
vertical-align: middle;
text-indent: calc(calc(#{$switch-height} * 2) + .5rem);
}
+ label::before,
+ label::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: calc(#{$switch-height} * 2);
bottom: 0;
display: block;
}
+ label::before {
right: 0;
background-color: $switch-bg;
border-radius: $switch-border-radius;
transition: $switch-transition;
}
+ label::after {
top: $switch-thumb-padding;
left: $switch-thumb-padding;
width: calc(#{$switch-height} - calc(#{$switch-thumb-padding} * 2));
height: calc(#{$switch-height} - calc(#{$switch-thumb-padding} * 2));
border-radius: $switch-thumb-border-radius;
background-color: $switch-thumb-bg;
transition: $switch-transition;
}
&:checked + label::before {
background-color: $switch-checked-bg;
}
&:checked + label::after {
margin-left: $switch-height;
}
&:focus + label::before {
outline: none;
box-shadow: $switch-focus-box-shadow;
}
&:disabled + label {
color: $switch-disabled-color;
cursor: not-allowed;
}
&:disabled + label::before {
background-color: $switch-disabled-bg;
}
}
// Small variation
&.switch-sm {
font-size: $font-size-sm;
input {
+ label {
min-width: calc(#{$switch-height-sm} * 2);
height: $switch-height-sm;
line-height: $switch-height-sm;
text-indent: calc(calc(#{$switch-height-sm} * 2) + .5rem);
}
+ label::before {
width: calc(#{$switch-height-sm} * 2);
}