diff --git a/config/__init__.py b/config/__init__.py index 1d61cd6..aca75cb 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1,7 +1,8 @@ -from .config import conf, STATIC_DIR +from .config import conf, STATIC_DIR, TEMPLATES_DIR __all__ = [ "conf", STATIC_DIR, + TEMPLATES_DIR, ] diff --git a/routers/__init__.py b/routers/__init__.py index d137e4b..6800f0f 100644 --- a/routers/__init__.py +++ b/routers/__init__.py @@ -2,7 +2,7 @@ from fastapi import APIRouter from .swagger import router as swagger_router from config import conf - +from .web import router as web_router router = APIRouter() @@ -10,3 +10,7 @@ router.include_router( swagger_router, prefix=conf.prefix.swagger, ) + +router.include_router( + web_router, +) diff --git a/routers/web/__init__.py b/routers/web/__init__.py new file mode 100644 index 0000000..734ac63 --- /dev/null +++ b/routers/web/__init__.py @@ -0,0 +1,22 @@ +from fastapi import APIRouter + +from .check_ports import router as check_ports_router +from .start_page import router as start_page_router +from .mikrotik_conf import router as mikrotik_conf_router + + +router = APIRouter( + tags=["Web"], +) +router.include_router( + check_ports_router, + prefix="/web/check_ports", +) +router.include_router( + mikrotik_conf_router, + prefix="/web/mikrotik_conf", +) +router.include_router( + start_page_router, + prefix="", +) diff --git a/routers/web/check_ports.py b/routers/web/check_ports.py new file mode 100644 index 0000000..1a68127 --- /dev/null +++ b/routers/web/check_ports.py @@ -0,0 +1,34 @@ +from fastapi import APIRouter +from starlette.requests import Request +from starlette.responses import HTMLResponse +from starlette.templating import Jinja2Templates +from config import TEMPLATES_DIR + +from scripts import check_available_ports + +router = APIRouter() + +templates = Jinja2Templates(directory=TEMPLATES_DIR) + + +@router.get("", response_class=HTMLResponse) +async def check_ports(request: Request): + return templates.TemplateResponse( + "body-check_ports.html", + { + "request": request, + }, + ) + + +@router.get("/start") +async def start_check_ports(request: Request, inputText: str): + + results = check_available_ports(inputText, result_type="text") + return templates.TemplateResponse( + "body-check_ports.html", + { + "request": request, + "results": results, + }, + ) diff --git a/routers/web/mikrotik_conf.py b/routers/web/mikrotik_conf.py new file mode 100644 index 0000000..40a08e6 --- /dev/null +++ b/routers/web/mikrotik_conf.py @@ -0,0 +1,49 @@ +from fastapi import APIRouter, Form +from starlette.requests import Request +from starlette.responses import HTMLResponse +from starlette.templating import Jinja2Templates +from config import TEMPLATES_DIR + +from scripts import check_available_ports + +router = APIRouter() + +templates = Jinja2Templates(directory=TEMPLATES_DIR) + + +@router.get("", response_class=HTMLResponse) +async def mikrotik_conf(request: Request): + return templates.TemplateResponse( + "mikrotik_conf/body-mikrotik_conf.html", + { + "request": request, + }, + ) + + +@router.get("/get-ports", response_class=HTMLResponse) +async def mikrotik_conf_generate(request: Request, model: str): + if model == "rb5009ug": + return templates.TemplateResponse( + "mikrotik_conf/ports_rb5009ug.html", + { + "request": request, + }, + ) + + +@router.get("/generate") +async def mikrotik_conf_generate(request: Request, inputText: str): + + return templates.TemplateResponse( + "mikrotik_conf/body-mikrotik_conf.html", + { + "request": request, + }, + ) + + +@router.post("/process-text") +async def test(inputText: str = Form(...)): + processed_text = inputText.upper() + return processed_text diff --git a/routers/web/start_page.py b/routers/web/start_page.py new file mode 100644 index 0000000..f433e6e --- /dev/null +++ b/routers/web/start_page.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter +from starlette.requests import Request +from starlette.responses import HTMLResponse +from starlette.templating import Jinja2Templates +from config import TEMPLATES_DIR + +router = APIRouter() + +templates = Jinja2Templates(directory=TEMPLATES_DIR) + + +@router.get("/", response_class=HTMLResponse) +async def start_page(request: Request): + return templates.TemplateResponse( + "base.html", + { + "request": request, + }, + ) diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..a231f06 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1,5 @@ +from .check_available_ports import start_check as check_available_ports + +__all__ = [ + "check_available_ports", +] diff --git a/scripts/check_available_ports.py b/scripts/check_available_ports.py new file mode 100644 index 0000000..9db0d16 --- /dev/null +++ b/scripts/check_available_ports.py @@ -0,0 +1,54 @@ +import socket +import re +from typing import List, Dict, Union +import logging + + +def check_conn(ip: str, port: int) -> bool: + logging.info("") + conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + conn.settimeout(3) + try: + conn.connect((ip, port)) + logging.info(f"{ip}:{port} - True") + return True + except TimeoutError: + logging.warning(f"{ip}:{port} - Timeout") + return False + + +def parse_input(text: str) -> List[Dict[str, Union[int, str]]]: + logging.info("") + pattern = r"Error;.*?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)/(\w+)" + matches = re.findall(pattern, text) + result = [ + {"ip": match[0], "port": int(match[1]), "protocol": match[2]} + for match in matches + ] + return result + + +def start_check( + text: str, + result_type: str = "json", +) -> List[Dict[str, Union[int, str, bool]]] | List[str]: + logging.info("") + result = [] + for i in parse_input(text): + if i["protocol"] == "tcp": + if check_conn(i["ip"], i["port"]): + i["result"] = True + result.append(i) + else: + i["result"] = False + result.append(i) + + if result_type == "text": + results = [] + for i in result: + if i["result"]: + results.append(f"{i['ip']}:{i['port']}/{i['protocol']} - OK") + else: + results.append(f"{i['ip']}:{i['port']}/{i['protocol']} - FAIL") + return results + return result diff --git a/web/static/css/base.css b/web/static/css/base.css new file mode 100644 index 0000000..edc884e --- /dev/null +++ b/web/static/css/base.css @@ -0,0 +1,29 @@ +header { +background-color: green; +position: relative; +top: 0; +left: 0; +width: 100%; +margin-bottom: 10px; +} + +div.header-button{ +#background-color: red; +display: inline-block; +float: right; + +} +div.body{ +position: relative; + +} + +footer { +background-color: red; +position:fixed; +left: 0; +bottom:0; +width:100%; +text-align: right; + +} diff --git a/web/static/js/htmx.js b/web/static/js/htmx.js new file mode 100644 index 0000000..5a5ad2b --- /dev/null +++ b/web/static/js/htmx.js @@ -0,0 +1,5130 @@ +var htmx = (function() { + 'use strict' + + // Public API + const htmx = { + // Tsc madness here, assigning the functions directly results in an invalid TypeScript output, but reassigning is fine + /* Event processing */ + /** @type {typeof onLoadHelper} */ + onLoad: null, + /** @type {typeof processNode} */ + process: null, + /** @type {typeof addEventListenerImpl} */ + on: null, + /** @type {typeof removeEventListenerImpl} */ + off: null, + /** @type {typeof triggerEvent} */ + trigger: null, + /** @type {typeof ajaxHelper} */ + ajax: null, + /* DOM querying helpers */ + /** @type {typeof find} */ + find: null, + /** @type {typeof findAll} */ + findAll: null, + /** @type {typeof closest} */ + closest: null, + /** + * Returns the input values that would resolve for a given element via the htmx value resolution mechanism + * + * @see https://htmx.org/api/#values + * + * @param {Element} elt the element to resolve values on + * @param {HttpVerb} type the request type (e.g. **get** or **post**) non-GET's will include the enclosing form of the element. Defaults to **post** + * @returns {Object} + */ + values: function(elt, type) { + const inputValues = getInputValues(elt, type || 'post') + return inputValues.values + }, + /* DOM manipulation helpers */ + /** @type {typeof removeElement} */ + remove: null, + /** @type {typeof addClassToElement} */ + addClass: null, + /** @type {typeof removeClassFromElement} */ + removeClass: null, + /** @type {typeof toggleClassOnElement} */ + toggleClass: null, + /** @type {typeof takeClassForElement} */ + takeClass: null, + /** @type {typeof swap} */ + swap: null, + /* Extension entrypoints */ + /** @type {typeof defineExtension} */ + defineExtension: null, + /** @type {typeof removeExtension} */ + removeExtension: null, + /* Debugging */ + /** @type {typeof logAll} */ + logAll: null, + /** @type {typeof logNone} */ + logNone: null, + /* Debugging */ + /** + * The logger htmx uses to log with + * + * @see https://htmx.org/api/#logger + */ + logger: null, + /** + * A property holding the configuration htmx uses at runtime. + * + * Note that using a [meta tag](https://htmx.org/docs/#config) is the preferred mechanism for setting these properties. + * + * @see https://htmx.org/api/#config + */ + config: { + /** + * Whether to use history. + * @type boolean + * @default true + */ + historyEnabled: true, + /** + * The number of pages to keep in **localStorage** for history support. + * @type number + * @default 10 + */ + historyCacheSize: 10, + /** + * @type boolean + * @default false + */ + refreshOnHistoryMiss: false, + /** + * The default swap style to use if **[hx-swap](https://htmx.org/attributes/hx-swap)** is omitted. + * @type HtmxSwapStyle + * @default 'innerHTML' + */ + defaultSwapStyle: 'innerHTML', + /** + * The default delay between receiving a response from the server and doing the swap. + * @type number + * @default 0 + */ + defaultSwapDelay: 0, + /** + * The default delay between completing the content swap and settling attributes. + * @type number + * @default 20 + */ + defaultSettleDelay: 20, + /** + * If true, htmx will inject a small amount of CSS into the page to make indicators invisible unless the **htmx-indicator** class is present. + * @type boolean + * @default true + */ + includeIndicatorStyles: true, + /** + * The class to place on indicators when a request is in flight. + * @type string + * @default 'htmx-indicator' + */ + indicatorClass: 'htmx-indicator', + /** + * The class to place on triggering elements when a request is in flight. + * @type string + * @default 'htmx-request' + */ + requestClass: 'htmx-request', + /** + * The class to temporarily place on elements that htmx has added to the DOM. + * @type string + * @default 'htmx-added' + */ + addedClass: 'htmx-added', + /** + * The class to place on target elements when htmx is in the settling phase. + * @type string + * @default 'htmx-settling' + */ + settlingClass: 'htmx-settling', + /** + * The class to place on target elements when htmx is in the swapping phase. + * @type string + * @default 'htmx-swapping' + */ + swappingClass: 'htmx-swapping', + /** + * Allows the use of eval-like functionality in htmx, to enable **hx-vars**, trigger conditions & script tag evaluation. Can be set to **false** for CSP compatibility. + * @type boolean + * @default true + */ + allowEval: true, + /** + * If set to false, disables the interpretation of script tags. + * @type boolean + * @default true + */ + allowScriptTags: true, + /** + * If set, the nonce will be added to inline scripts. + * @type string + * @default '' + */ + inlineScriptNonce: '', + /** + * If set, the nonce will be added to inline styles. + * @type string + * @default '' + */ + inlineStyleNonce: '', + /** + * The attributes to settle during the settling phase. + * @type string[] + * @default ['class', 'style', 'width', 'height'] + */ + attributesToSettle: ['class', 'style', 'width', 'height'], + /** + * Allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates. + * @type boolean + * @default false + */ + withCredentials: false, + /** + * @type number + * @default 0 + */ + timeout: 0, + /** + * The default implementation of **getWebSocketReconnectDelay** for reconnecting after unexpected connection loss by the event code **Abnormal Closure**, **Service Restart** or **Try Again Later**. + * @type {'full-jitter' | ((retryCount:number) => number)} + * @default "full-jitter" + */ + wsReconnectDelay: 'full-jitter', + /** + * The type of binary data being received over the WebSocket connection + * @type BinaryType + * @default 'blob' + */ + wsBinaryType: 'blob', + /** + * @type string + * @default '[hx-disable], [data-hx-disable]' + */ + disableSelector: '[hx-disable], [data-hx-disable]', + /** + * @type {'auto' | 'instant' | 'smooth'} + * @default 'smooth' + */ + scrollBehavior: 'instant', + /** + * If the focused element should be scrolled into view. + * @type boolean + * @default false + */ + defaultFocusScroll: false, + /** + * If set to true htmx will include a cache-busting parameter in GET requests to avoid caching partial responses by the browser + * @type boolean + * @default false + */ + getCacheBusterParam: false, + /** + * If set to true, htmx will use the View Transition API when swapping in new content. + * @type boolean + * @default false + */ + globalViewTransitions: false, + /** + * htmx will format requests with these methods by encoding their parameters in the URL, not the request body + * @type {(HttpVerb)[]} + * @default ['get', 'delete'] + */ + methodsThatUseUrlParams: ['get', 'delete'], + /** + * If set to true, disables htmx-based requests to non-origin hosts. + * @type boolean + * @default false + */ + selfRequestsOnly: true, + /** + * If set to true htmx will not update the title of the document when a title tag is found in new content + * @type boolean + * @default false + */ + ignoreTitle: false, + /** + * Whether the target of a boosted element is scrolled into the viewport. + * @type boolean + * @default true + */ + scrollIntoViewOnBoost: true, + /** + * The cache to store evaluated trigger specifications into. + * You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + * @type {Object|null} + * @default null + */ + triggerSpecsCache: null, + /** @type boolean */ + disableInheritance: false, + /** @type HtmxResponseHandlingConfig[] */ + responseHandling: [ + { code: '204', swap: false }, + { code: '[23]..', swap: true }, + { code: '[45]..', swap: false, error: true } + ], + /** + * Whether to process OOB swaps on elements that are nested within the main response element. + * @type boolean + * @default true + */ + allowNestedOobSwaps: true + }, + /** @type {typeof parseInterval} */ + parseInterval: null, + /** @type {typeof internalEval} */ + _: null, + version: '2.0.1' + } + // Tsc madness part 2 + htmx.onLoad = onLoadHelper + htmx.process = processNode + htmx.on = addEventListenerImpl + htmx.off = removeEventListenerImpl + htmx.trigger = triggerEvent + htmx.ajax = ajaxHelper + htmx.find = find + htmx.findAll = findAll + htmx.closest = closest + htmx.remove = removeElement + htmx.addClass = addClassToElement + htmx.removeClass = removeClassFromElement + htmx.toggleClass = toggleClassOnElement + htmx.takeClass = takeClassForElement + htmx.swap = swap + htmx.defineExtension = defineExtension + htmx.removeExtension = removeExtension + htmx.logAll = logAll + htmx.logNone = logNone + htmx.parseInterval = parseInterval + htmx._ = internalEval + + const internalAPI = { + addTriggerHandler, + bodyContains, + canAccessLocalStorage, + findThisElement, + filterValues, + swap, + hasAttribute, + getAttributeValue, + getClosestAttributeValue, + getClosestMatch, + getExpressionVars, + getHeaders, + getInputValues, + getInternalData, + getSwapSpecification, + getTriggerSpecs, + getTarget, + makeFragment, + mergeObjects, + makeSettleInfo, + oobSwap, + querySelectorExt, + settleImmediately, + shouldCancel, + triggerEvent, + triggerErrorEvent, + withExtensions + } + + const VERBS = ['get', 'post', 'put', 'delete', 'patch'] + const VERB_SELECTOR = VERBS.map(function(verb) { + return '[hx-' + verb + '], [data-hx-' + verb + ']' + }).join(', ') + + const HEAD_TAG_REGEX = makeTagRegEx('head') + + //= =================================================================== + // Utilities + //= =================================================================== + + /** + * @param {string} tag + * @param {boolean} global + * @returns {RegExp} + */ + function makeTagRegEx(tag, global = false) { + return new RegExp(`<${tag}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${tag}>`, + global ? 'gim' : 'im') + } + + /** + * Parses an interval string consistent with the way htmx does. Useful for plugins that have timing-related attributes. + * + * Caution: Accepts an int followed by either **s** or **ms**. All other values use **parseFloat** + * + * @see https://htmx.org/api/#parseInterval + * + * @param {string} str timing string + * @returns {number|undefined} + */ + function parseInterval(str) { + if (str == undefined) { + return undefined + } + + let interval = NaN + if (str.slice(-2) == 'ms') { + interval = parseFloat(str.slice(0, -2)) + } else if (str.slice(-1) == 's') { + interval = parseFloat(str.slice(0, -1)) * 1000 + } else if (str.slice(-1) == 'm') { + interval = parseFloat(str.slice(0, -1)) * 1000 * 60 + } else { + interval = parseFloat(str) + } + return isNaN(interval) ? undefined : interval + } + + /** + * @param {Node} elt + * @param {string} name + * @returns {(string | null)} + */ + function getRawAttribute(elt, name) { + return elt instanceof Element && elt.getAttribute(name) + } + + /** + * @param {Element} elt + * @param {string} qualifiedName + * @returns {boolean} + */ + // resolve with both hx and data-hx prefixes + function hasAttribute(elt, qualifiedName) { + return !!elt.hasAttribute && (elt.hasAttribute(qualifiedName) || + elt.hasAttribute('data-' + qualifiedName)) + } + + /** + * + * @param {Node} elt + * @param {string} qualifiedName + * @returns {(string | null)} + */ + function getAttributeValue(elt, qualifiedName) { + return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, 'data-' + qualifiedName) + } + + /** + * @param {Node} elt + * @returns {Node | null} + */ + function parentElt(elt) { + const parent = elt.parentElement + if (!parent && elt.parentNode instanceof ShadowRoot) return elt.parentNode + return parent + } + + /** + * @returns {Document} + */ + function getDocument() { + return document + } + + /** + * @param {Node} elt + * @param {boolean} global + * @returns {Node|Document} + */ + function getRootNode(elt, global) { + return elt.getRootNode ? elt.getRootNode({ composed: global }) : getDocument() + } + + /** + * @param {Node} elt + * @param {(e:Node) => boolean} condition + * @returns {Node | null} + */ + function getClosestMatch(elt, condition) { + while (elt && !condition(elt)) { + elt = parentElt(elt) + } + + return elt || null + } + + /** + * @param {Element} initialElement + * @param {Element} ancestor + * @param {string} attributeName + * @returns {string|null} + */ + function getAttributeValueWithDisinheritance(initialElement, ancestor, attributeName) { + const attributeValue = getAttributeValue(ancestor, attributeName) + const disinherit = getAttributeValue(ancestor, 'hx-disinherit') + var inherit = getAttributeValue(ancestor, 'hx-inherit') + if (initialElement !== ancestor) { + if (htmx.config.disableInheritance) { + if (inherit && (inherit === '*' || inherit.split(' ').indexOf(attributeName) >= 0)) { + return attributeValue + } else { + return null + } + } + if (disinherit && (disinherit === '*' || disinherit.split(' ').indexOf(attributeName) >= 0)) { + return 'unset' + } + } + return attributeValue + } + + /** + * @param {Element} elt + * @param {string} attributeName + * @returns {string | null} + */ + function getClosestAttributeValue(elt, attributeName) { + let closestAttr = null + getClosestMatch(elt, function(e) { + return !!(closestAttr = getAttributeValueWithDisinheritance(elt, asElement(e), attributeName)) + }) + if (closestAttr !== 'unset') { + return closestAttr + } + } + + /** + * @param {Node} elt + * @param {string} selector + * @returns {boolean} + */ + function matches(elt, selector) { + // @ts-ignore: non-standard properties for browser compatibility + // noinspection JSUnresolvedVariable + const matchesFunction = elt instanceof Element && (elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector) + return !!matchesFunction && matchesFunction.call(elt, selector) + } + + /** + * @param {string} str + * @returns {string} + */ + function getStartTag(str) { + const tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i + const match = tagMatcher.exec(str) + if (match) { + return match[1].toLowerCase() + } else { + return '' + } + } + + /** + * @param {string} resp + * @returns {Document} + */ + function parseHTML(resp) { + const parser = new DOMParser() + return parser.parseFromString(resp, 'text/html') + } + + /** + * @param {DocumentFragment} fragment + * @param {Node} elt + */ + function takeChildrenFor(fragment, elt) { + while (elt.childNodes.length > 0) { + fragment.append(elt.childNodes[0]) + } + } + + /** + * @param {HTMLScriptElement} script + * @returns {HTMLScriptElement} + */ + function duplicateScript(script) { + const newScript = getDocument().createElement('script') + forEach(script.attributes, function(attr) { + newScript.setAttribute(attr.name, attr.value) + }) + newScript.textContent = script.textContent + newScript.async = false + if (htmx.config.inlineScriptNonce) { + newScript.nonce = htmx.config.inlineScriptNonce + } + return newScript + } + + /** + * @param {HTMLScriptElement} script + * @returns {boolean} + */ + function isJavaScriptScriptNode(script) { + return script.matches('script') && (script.type === 'text/javascript' || script.type === 'module' || script.type === '') + } + + /** + * we have to make new copies of script tags that we are going to insert because + * SOME browsers (not saying who, but it involves an element and an animal) don't + * execute scripts created in