diff --git a/package-lock.json b/package-lock.json index 0ea6847..c40fba8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "txtdot", - "version": "1.1.1", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "txtdot", - "version": "1.1.1", + "version": "1.2.0", "license": "MIT", "dependencies": { "@fastify/static": "^6.10.2", diff --git a/package.json b/package.json index 5f4ba7d..39dd3ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "txtdot", - "version": "1.1.1", + "version": "1.2.0", "private": true, "description": "", "main": "dist/app.js", diff --git a/src/errors/api.ts b/src/errors/api.ts new file mode 100644 index 0000000..4d0728c --- /dev/null +++ b/src/errors/api.ts @@ -0,0 +1,34 @@ +export interface IApiError { + code: number; + name: string; + message: string; +} + +export const errorSchema = { + type: "object", + properties: { + code: { + type: "number", + description: "HTTP error code", + }, + name: { + type: "string", + description: "Exception class name", + }, + message: { + type: "string", + description: "Exception message", + }, + }, +}; + +export const errorResponseSchema = { + type: "object", + properties: { + data: { + type: "object", + nullable: true, + }, + error: errorSchema, + }, +}; diff --git a/src/errors/handler.ts b/src/errors/handler.ts index 58365ae..b23560b 100644 --- a/src/errors/handler.ts +++ b/src/errors/handler.ts @@ -1,14 +1,66 @@ import { FastifyReply, FastifyRequest } from "fastify"; -import { NotHtmlMimetypeError } from "./main"; +import { NotHtmlMimetypeError, TxtDotError } from "./main"; +import { getFastifyError } from "./validation"; export default function errorHandler( error: Error, - _: FastifyRequest, + req: FastifyRequest, reply: FastifyReply ) { + if (req.originalUrl.startsWith("/api/")) { + return apiErrorHandler(error, reply); + } + return htmlErrorHandler(error, reply); +} + +function apiErrorHandler(error: Error, reply: FastifyReply) { + function generateResponse(code: number) { + return reply.code(code).send({ + data: null, + error: { + code: code, + name: error.name, + message: error.message, + }, + }); + } + + if (error instanceof NotHtmlMimetypeError) { + return generateResponse(501); + } + + if (getFastifyError(error)?.statusCode === 400) { + return generateResponse(400); + } + + if (error instanceof TxtDotError) { + return generateResponse(error.code); + } + + return generateResponse(500); +} + +function htmlErrorHandler(error: Error, reply: FastifyReply) { if (error instanceof NotHtmlMimetypeError) { return reply.redirect(301, error.url); - } else { - return error; } + + if (getFastifyError(error)?.statusCode === 400) { + return reply.code(400).view("/templates/error.ejs", { + code: 400, + description: `Invalid parameter specified: ${error.message}`, + }) + } + + if (error instanceof TxtDotError) { + return reply.code(error.code).view("/templates/error.ejs", { + code: error.code, + description: error.description, + }); + } + + return reply.code(500).view("/templates/error.ejs", { + code: 500, + description: `${error.name}: ${error.message}`, + }); } diff --git a/src/errors/main.ts b/src/errors/main.ts index bd528bd..29190c9 100644 --- a/src/errors/main.ts +++ b/src/errors/main.ts @@ -1,10 +1,34 @@ -export class EngineParseError extends Error {} -export class InvalidParameterError extends Error {} -export class LocalResourceError extends Error {} -export class NotHtmlMimetypeError extends Error { - url: string; - constructor(params: { url: string }) { - super(); - this.url = params?.url; +export abstract class TxtDotError extends Error { + code: number; + name: string; + description: string; + + constructor(code: number, name: string, description: string) { + super(description); + this.code = code; + this.name = name; + this.description = description; + } +} + +export class EngineParseError extends TxtDotError { + constructor(message: string) { + super(422, "EngineParseError", `Parse error: ${message}`); + } +} + +export class LocalResourceError extends TxtDotError { + constructor() { + super(403, "LocalResourceError", "Proxying local resources is forbidden."); + } +} + +export class NotHtmlMimetypeError extends Error { + name: string = "NotHtmlMimetypeError"; + url: string; + + constructor(url: string) { + super(); + this.url = url; } } diff --git a/src/errors/validation.ts b/src/errors/validation.ts new file mode 100644 index 0000000..5304da8 --- /dev/null +++ b/src/errors/validation.ts @@ -0,0 +1,9 @@ +export interface IFastifyValidationError { + statusCode?: number; + code?: string; + validation?: any; +} + +export function getFastifyError(error: Error) { + return error as unknown as IFastifyValidationError; +} diff --git a/src/handlers/main.ts b/src/handlers/main.ts index 0fbb172..7a37f5b 100644 --- a/src/handlers/main.ts +++ b/src/handlers/main.ts @@ -6,16 +6,15 @@ import { DOMWindow } from "jsdom"; import readability from "./readability"; import google from "./google"; +import stackoverflow from "./stackoverflow/main"; import { generateProxyUrl } from "../utils/generate"; import isLocalResource from "../utils/islocal"; import { - InvalidParameterError, LocalResourceError, NotHtmlMimetypeError, } from "../errors/main"; -import stackoverflow from "./stackoverflow/main"; export default async function handlePage( url: string, // remote URL @@ -28,21 +27,21 @@ export default async function handlePage( throw new LocalResourceError(); } - if (engine && engineList.indexOf(engine) === -1) { - throw new InvalidParameterError("Invalid engine"); - } - const response = await axios.get(url); const mime: string | undefined = response.headers["content-type"]?.toString(); if (mime && mime.indexOf("text/html") === -1) { - throw new NotHtmlMimetypeError({ url }); + throw new NotHtmlMimetypeError(url); } const window = new JSDOM(response.data, { url }).window; [...window.document.getElementsByTagName("a")].forEach((link) => { - link.href = generateProxyUrl(requestUrl, link.href, engine); + try { + link.href = generateProxyUrl(requestUrl, link.href, engine); + } catch (_err) { + // ignore TypeError: Invalid URL + } }); if (engine) { diff --git a/src/publicConfig.ts b/src/publicConfig.ts index bb34e36..847469e 100644 --- a/src/publicConfig.ts +++ b/src/publicConfig.ts @@ -1,5 +1,5 @@ export default { - version: "1.1.0", + version: "1.1.1", description: "HTTP proxy that parses only text, links and pictures from pages reducing internet traffic, removing ads and heavy scripts", }; diff --git a/src/routes/get.ts b/src/routes/get.ts index dbd1e59..795b7e2 100644 --- a/src/routes/get.ts +++ b/src/routes/get.ts @@ -1,6 +1,6 @@ import { FastifyInstance } from "fastify"; -import { GetSchema, IGetSchema } from "../types/requests"; +import { GetSchema, IGetSchema } from "../types/requests/browser"; import handlePage from "../handlers/main"; import { generateRequestUrl } from "../utils/generate"; diff --git a/src/routes/index.ts b/src/routes/index.ts index ea3ff2d..00d6dba 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,6 +1,6 @@ import { FastifyInstance } from "fastify"; import { engineList } from "../handlers/main"; -import { indexSchema } from "../types/requests"; +import { indexSchema } from "../types/requests/browser"; export default async function indexRoute(fastify: FastifyInstance) { fastify.get("/", { schema: indexSchema }, async (_, reply) => { diff --git a/src/routes/parse.ts b/src/routes/parse.ts index 24b5738..bca7d1d 100644 --- a/src/routes/parse.ts +++ b/src/routes/parse.ts @@ -1,5 +1,7 @@ -import { EngineRequest, IParseSchema, parseSchema } from "../types/requests"; import { FastifyInstance } from "fastify"; + +import { EngineRequest, IParseSchema, parseSchema } from "../types/requests/api"; + import handlePage from "../handlers/main"; import { generateRequestUrl } from "../utils/generate"; @@ -8,15 +10,18 @@ export default async function parseRoute(fastify: FastifyInstance) { "/api/parse", { schema: parseSchema }, async (request: EngineRequest) => { - return await handlePage( - request.query.url, - generateRequestUrl( - request.protocol, - request.hostname, - request.originalUrl + return { + data: await handlePage( + request.query.url, + generateRequestUrl( + request.protocol, + request.hostname, + request.originalUrl + ), + request.query.engine ), - request.query.engine - ); + error: null, + }; } ); } diff --git a/src/routes/raw-html.ts b/src/routes/raw-html.ts index 83ac62f..4665fe2 100644 --- a/src/routes/raw-html.ts +++ b/src/routes/raw-html.ts @@ -1,6 +1,8 @@ import { FastifyInstance } from "fastify"; -import { GetRequest, IParseSchema, rawHtmlSchema } from "../types/requests"; +import { IParseSchema, rawHtmlSchema } from "../types/requests/api"; +import { GetRequest } from "../types/requests/browser"; + import handlePage from "../handlers/main"; import { generateRequestUrl } from "../utils/generate"; diff --git a/src/types/requests.ts b/src/types/requests.ts deleted file mode 100644 index e395149..0000000 --- a/src/types/requests.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { FastifyRequest, FastifySchema } from "fastify"; -import { handlerSchema } from "../handlers/handler.interface"; -import { engineList } from "../handlers/main"; - -export type GetRequest = FastifyRequest<{ - Querystring: { - url: string; - format?: string; - engine?: string; - }; -}>; - -export interface IGetQuery { - url: string; - format?: string; - engine?: string; -} - -export interface IParseQuery { - url: string; - engine?: string; -} - -export interface IGetSchema { - Querystring: IGetQuery; -} - -export interface IParseSchema { - Querystring: IParseQuery; -} - -export const indexSchema = { - produces: ["text/html"], - hide: true -}; - -export const getQuerySchema = { - type: "object", - required: ["url"], - properties: { - url: { - type: "string", - description: "URL", - }, - format: { - type: "string", - enum: ["text", "html", ""], - default: "html", - }, - engine: { - type: "string", - enum: [...engineList, ""], - }, - }, -}; - -export const parseQuerySchema = { - type: "object", - required: ["url"], - properties: { - url: { - type: "string", - description: "URL", - }, - engine: { - type: "string", - enum: [...engineList, ""], - }, - }, -}; - -export const GetSchema: FastifySchema = { - description: "Get page", - hide: true, - querystring: getQuerySchema, - produces: ["text/html", "text/plain"], -}; - -export const parseSchema: FastifySchema = { - description: "Parse page", - querystring: parseQuerySchema, - response: { - "2xx": handlerSchema, - }, - produces: ["text/json"], -}; - -export const rawHtmlSchema: FastifySchema = { - description: "Get raw HTML", - querystring: parseQuerySchema, - produces: ["text/html"], -}; - -export type EngineRequest = FastifyRequest<{ - Querystring: { - url: string; - engine?: string; - }; -}>; diff --git a/src/types/requests/api.ts b/src/types/requests/api.ts new file mode 100644 index 0000000..9e13803 --- /dev/null +++ b/src/types/requests/api.ts @@ -0,0 +1,66 @@ +import { FastifySchema, FastifyRequest } from "fastify"; +import { IApiError, errorResponseSchema } from "../../errors/api"; +import { handlerSchema } from "../../handlers/handler.interface"; +import { engineList } from "../../handlers/main"; + +export interface IApiResponse { + data?: T; + error?: IApiError; +} + +export interface IParseQuery { + url: string; + engine?: string; +} + +export interface IParseSchema { + Querystring: IParseQuery; +} + +export const parseQuerySchema = { + type: "object", + required: ["url"], + properties: { + url: { + type: "string", + description: "URL", + }, + engine: { + type: "string", + enum: [...engineList, ""], + }, + }, +}; + +export const parseSchema: FastifySchema = { + description: "Parse the page and get all data from the engine", + querystring: parseQuerySchema, + response: { + "2xx": { + type: "object", + properties: { + data: handlerSchema, + error: { + type: "object", + nullable: true, + }, + }, + }, + "4xx": errorResponseSchema, + "5xx": errorResponseSchema, + }, + produces: ["text/json"], +}; + +export const rawHtmlSchema: FastifySchema = { + description: "Parse the page and get raw HTML from the engine", + querystring: parseQuerySchema, + produces: ["text/html"], +}; + +export type EngineRequest = FastifyRequest<{ + Querystring: { + url: string; + engine?: string; + }; +}>; diff --git a/src/types/requests/browser.ts b/src/types/requests/browser.ts new file mode 100644 index 0000000..df526d9 --- /dev/null +++ b/src/types/requests/browser.ts @@ -0,0 +1,48 @@ +import { FastifyRequest, FastifySchema } from "fastify"; +import { engineList } from "../../handlers/main"; + +export type GetRequest = FastifyRequest<{ + Querystring: IGetQuery; +}>; + +export interface IGetQuery { + url: string; + format?: string; + engine?: string; +} + +export interface IGetSchema { + Querystring: IGetQuery; +} + +export const indexSchema = { + produces: ["text/html"], + hide: true +}; + +export const getQuerySchema = { + type: "object", + required: ["url"], + properties: { + url: { + type: "string", + description: "URL", + }, + format: { + type: "string", + enum: ["text", "html", ""], + default: "html", + }, + engine: { + type: "string", + enum: [...engineList, ""], + }, + }, +}; + +export const GetSchema: FastifySchema = { + description: "Get page", + hide: true, + querystring: getQuerySchema, + produces: ["text/html", "text/plain"], +}; diff --git a/static/common.css b/static/common.css index 7c455f4..f8a6360 100644 --- a/static/common.css +++ b/static/common.css @@ -9,8 +9,10 @@ --bg2: #bbb; --fg2: #333; - --accent: hsl(207, 100%, 40%); - --accent-hl: hsl(207, 100%, 20%); + --accent: #0070cc; /* hsl(207, 100%, 40%) */ + --accent-hl: #003866; /* hsl(207, 100%, 20%) */ + + --error: #ff9400; } @media (prefers-color-scheme: dark) { @@ -21,8 +23,8 @@ --bg2: #444; --fg2: #bbb; - --accent: hsl(207, 100%, 60%); - --accent-hl: hsl(207, 100%, 80%); + --accent: #33a3ff; /* hsl(207, 100%, 60%) */ + --accent-hl: #99d1ff; /* hsl(207, 100%, 80%) */ } } diff --git a/static/form.css b/static/form.css new file mode 100644 index 0000000..67e9cd7 --- /dev/null +++ b/static/form.css @@ -0,0 +1,60 @@ +.input-grid { + display: grid; + /* 2 columns: auto width, min-content width */ + grid-template-columns: auto min-content; + + /* gap: row column */ + gap: 0.5rem 0.25rem; + + width: fit-content; +} + +.input-row { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.75rem; +} + +.input { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.25rem; +} + +label { + font-size: 0.9rem; +} + +#url { + width: 100%; + height: 100%; /* shrink to #submit height */ + + outline: none; + border: 0; + border-bottom: 0.125rem solid var(--fg2); + + background: var(--bg); + color: var(--fg); + font-size: 1rem; +} +#url::placeholder { + color: var(--fg2); + opacity: 1; +} + +#submit { + font-size: 1rem; +} + +select { + border: 0; + border-bottom: 0.125rem solid var(--accent); + + background: var(--bg); + color: var(--fg); + + font-weight: 500; + font-size: 0.9rem; +} diff --git a/static/get.css b/static/get.css index 2ad8a71..b1ba611 100644 --- a/static/get.css +++ b/static/get.css @@ -5,9 +5,10 @@ font-size: 0.9rem; } + .title { + border-bottom: 0.125rem solid var(--bg2); font-weight: 600; - margin: 1rem; } a { diff --git a/static/index.css b/static/index.css index 5402aa2..5a2d551 100644 --- a/static/index.css +++ b/static/index.css @@ -2,73 +2,17 @@ main { display: flex; flex-direction: column; align-items: center; + text-align: center; } h1 { width: fit-content; margin: auto; } -h1 > span { + +h1 > .dot { color: var(--accent); } - -.input-grid { - display: grid; - /* 2 columns: auto width, min-content width */ - grid-template-columns: auto min-content; - - /* gap: row column */ - gap: 0.5rem 0.25rem; - - width: fit-content; -} - -.input-row { - display: flex; - flex-direction: row; - align-items: center; - gap: 0.75rem; -} - -.input { - display: flex; - flex-direction: row; - align-items: center; - gap: 0.25rem; -} - -label { - font-size: 0.9rem; -} - -#url { - width: 100%; - height: 100%; /* shrink to #submit height */ - - outline: none; - border: 0; - border-bottom: 0.125rem solid var(--fg2); - - background: var(--bg); - color: var(--fg); - font-size: 1rem; -} -#url::placeholder { - color: var(--fg2); - opacity: 1; -} - -#submit { - font-size: 1rem; -} - -select { - border: 0; - border-bottom: 0.125rem solid var(--accent); - - background: var(--bg); - color: var(--fg); - - font-weight: 600; - font-size: 0.9rem; +h1 > .dot-err { + color: var(--error); } diff --git a/templates/error.ejs b/templates/error.ejs new file mode 100644 index 0000000..10aba6b --- /dev/null +++ b/templates/error.ejs @@ -0,0 +1,21 @@ + + + + + + + + txt. <%= code %> + + + + +
+
+

txt.

+

<%= description %>

+
+ Home +
+ + diff --git a/templates/get.ejs b/templates/get.ejs index 20cbf27..6e7d18d 100644 --- a/templates/get.ejs +++ b/templates/get.ejs @@ -15,10 +15,9 @@ Home Original page -
+

<%= parsed.title %> -

-
+

<%- parsed.content %> diff --git a/templates/index.ejs b/templates/index.ejs index 70e1c72..d4c7528 100644 --- a/templates/index.ejs +++ b/templates/index.ejs @@ -9,11 +9,12 @@ txt. main page +
-

txt.

+

txt.

<%= desc %>