Compare commits

...

2 Commits

12 changed files with 400 additions and 129 deletions

4
.gitignore vendored
View File

@ -5,6 +5,8 @@
/.pnp
.pnp.js
/.local/
# testing
/coverage
@ -25,7 +27,7 @@ yarn-debug.log*
yarn-error.log*
# local env files
.env*
.env.development
# vercel
.vercel

2
.tool-versions Normal file
View File

@ -0,0 +1,2 @@
nodejs 20.8.1
pnpm 8.9.2

View File

@ -0,0 +1,109 @@
import {ReactElement} from "react";
import {domToReact, Element} from "html-react-parser";
import {nunito_sans, raleway} from "@/app/fonts";
import {Code} from "bright";
import {mark} from "@/bright-extension/mark";
export type nodeHandlerResult = {match :boolean, element? :ReactElement}
export type nodeHandler = (node :Element) => nodeHandlerResult
function h1(node: Element): nodeHandlerResult {
if (node.name === "h1") {
if (node.attribs.class === "title") {
return {
match: true,
element: (
<h1 className={`${raleway.className} mx-auto w-224 text-4xl`}>
{domToReact(node.children)}
</h1>
),
};
}
return {
match: true,
element: (
<h1 className={`${raleway.className} mx-auto w-224 text-3xl`}>
{domToReact(node.children)}
</h1>
),
};
}
return { match: false};
}
function h2(node: Element): nodeHandlerResult {
if (node.name === "h2") {
return {
match: true,
element: (
<h1 className={`${raleway.className} mx-auto w-224 text-2xl`}>
{domToReact(node.children)}
</h1>
)
};
}
return {match: false};
}
function h3(node: Element): nodeHandlerResult {
if (node.name === "h3") {
return {
match: true,
element: (
<h1 className={`${raleway.className} mx-auto w-224 text-xl`}>
{domToReact(node.children)}
</h1>
)
};
}
return {match: false};
}
function code(node: Element): nodeHandlerResult {
if (node.name === "code") {
let linenumber = false;
if ("class" in node.attribs) {
const classes = node.attribs.class.split(" ");
if (classes.includes("linenumber")) {
linenumber = true;
}
}
return {
match: true,
element: (
<div className={`w-224 mx-auto`}>
<Code
lang={`${node.attribs.lang}`}
lineNumbers={linenumber}
extensions={[mark]}
>
{domToReact(node.children)}
</Code>
</div>
)
};
}
return {match: false};
}
function p(node: Element): nodeHandlerResult {
if (node.name === "p") {
if (node.attribs.class === "paragraph") {
return {
match: true,
element: (
<h1 className={`${nunito_sans.className} mx-auto w-224`}>
{domToReact(node.children)}
</h1>
)
};
}
}
return {match: false};
}
export const handlers: nodeHandler[] = [h1, h2, h3, code, p];

View File

@ -1,62 +1,21 @@
import { getPost } from "@/backend/post";
import DOMPurify from "dompurify";
import { JSDOM } from "jsdom";
import { nunito_sans, raleway } from "@/app/fonts";
import parse, {
domToReact,
Element,
// domToReact,
HTMLReactParserOptions,
} from "html-react-parser";
import { DummyPostSlug, DummyPostString } from "@/components/dummyPost";
import { handlers } from "@/app/post/[slug]/nodeHandler";
const options: HTMLReactParserOptions = {
replace: (domNode) => {
if (domNode instanceof Element && domNode.attribs) {
// console.log(domNode.attribs)
if (domNode.name === "h1") {
if (domNode.attribs.class === "title") {
return (
<h1 className={`${raleway.className} mx-auto w-224 text-4xl`}>
{domToReact(domNode.children)}
</h1>
);
} else {
return (
<h1 className={`${raleway.className} mx-auto w-224 text-3xl`}>
{domToReact(domNode.children)}
</h1>
);
}
} else if (domNode.name === "h2") {
return (
<h1 className={`${raleway.className} mx-auto w-224 text-2xl`}>
{domToReact(domNode.children)}
</h1>
);
} else if (domNode.name === "h3") {
return (
<h1 className={`${raleway.className} mx-auto w-224 text-xl`}>
{domToReact(domNode.children)}
</h1>
);
} else if (domNode.name === "p") {
if (domNode.attribs.class === "paragraph") {
return (
<h1 className={`${nunito_sans.className} mx-auto w-224`}>
{domToReact(domNode.children)}
</h1>
);
} else {
const classes = domNode.attribs.class.split(" ");
if (classes.includes("code")) {
if (classes.includes("shell")) {
} else if (classes.includes("go") || classes.includes("golang")) {
} else {
}
}
for (let handler of handlers) {
if (domNode instanceof Element && domNode.attribs) {
let result = handler(domNode)
if (result.match && typeof result.element !== 'undefined') {
return result.element
}
}
}
@ -64,12 +23,12 @@ const options: HTMLReactParserOptions = {
};
export default async function Post({ params }: { params: { slug: string } }) {
let content = "";
let content;
const dummySlug = await DummyPostSlug();
if (dummySlug === params.slug) {
content = await DummyPostString();
console.log(content);
// console.log(content);
} else {
content = await getPost(params.slug);
}
@ -78,5 +37,9 @@ export default async function Post({ params }: { params: { slug: string } }) {
// console.log(content)
const elem = parse(content, options);
return <div className={`flex flex-col`}>{elem}</div>;
return (
<div className={`flex flex-col`}>
{elem}
</div>
);
}

View File

@ -1,11 +1,25 @@
import mysql, { PoolOptions } from "mysql2";
import * as fs from 'fs';
import * as appEnv from "./env";
if (typeof process.env.MYSQL_SSL_CA === 'undefined') {
throw new Error("missing MYSQL_SSL_CA")
}
if (typeof process.env.MYSQL_SSL_KEY === 'undefined') {
throw new Error("missing MYSQL_SSL_KEY")
}
if (typeof process.env.MYSQL_SSL_CERT === 'undefined') {
throw new Error("missing MYSQL_SSL_CERT")
}
const access: PoolOptions = {
host: process.env.MYSQL_HOST,
port: 'MYSQL_PORT' in process.env && typeof process.env.MYSQL_PORT === 'string' ? parseInt(process.env.MYSQL_PORT) : 3306,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: 'MYSQL_DATABASE' in process.env ? process.env.MYSQL_DATABASE : 'blog',
host: appEnv.getMysqlHost(),
port: appEnv.getMysqlPort(),
user: appEnv.getMysqlUser(),
password: appEnv.getMysqlPassword(),
database: appEnv.getMysqlDatabase(),
waitForConnections: true,
connectionLimit: 10,
maxIdle: 10,
@ -13,6 +27,11 @@ const access: PoolOptions = {
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 0,
ssl: {
ca: fs.readFileSync(appEnv.getMysqlSslCaFile()),
key: fs.readFileSync(appEnv.getMysqlSslKeyFile()),
cert: fs.readFileSync(appEnv.getMysqlSslCertFile())
}
}
export const pool = mysql.createPool(access)

63
backend/env.ts Normal file
View File

@ -0,0 +1,63 @@
export function getMysqlHost(): string {
if (typeof process.env.MYSQL_HOST === 'undefined') {
throw new Error("missing env MYSQL_HOST")
}
return process.env.MYSQL_HOST
}
export function getMysqlPort(): number {
if (typeof process.env.MYSQL_PORT === 'undefined') {
throw new Error("missing env MYSQL_PORT")
}
return parseInt(process.env.MYSQL_PORT)
}
export function getMysqlUser(): string {
if (typeof process.env.MYSQL_USER === 'undefined') {
throw new Error("missing env MYSQL_USER")
}
return process.env.MYSQL_USER
}
export function getMysqlPassword(): string {
if (typeof process.env.MYSQL_PASSWORD === 'undefined') {
throw new Error("missing env MYSQL_PASSWORD")
}
return process.env.MYSQL_PASSWORD
}
export function getMysqlDatabase(): string {
if (typeof process.env.MYSQL_DATABASE === 'undefined') {
throw new Error("missing env MYSQL_DATABASE")
}
return process.env.MYSQL_DATABASE
}
export function getMysqlSslCaFile(): string {
if (typeof process.env.MYSQL_SSL_CA === 'undefined') {
throw new Error("missing env MYSQL_SSL_CA")
}
return process.env.MYSQL_SSL_CA
}
export function getMysqlSslCertFile(): string {
if (typeof process.env.MYSQL_SSL_CERT === 'undefined') {
throw new Error("missing env MYSQL_SSL_CERT")
}
return process.env.MYSQL_SSL_CERT
}
export function getMysqlSslKeyFile(): string {
if (typeof process.env.MYSQL_SSL_KEY === 'undefined') {
throw new Error("missing env MYSQL_SSL_KEY")
}
return process.env.MYSQL_SSL_KEY
}

View File

@ -0,0 +1,11 @@
import { Extension } from 'bright';
export const lineNumbers :Extension = {
name: "lineNumbers",
beforeHighlight: (props, annotations) => {
console.log(annotations);
if (annotations.length > 0 ) {
return { ...props, lineNumbers: true }
}
}
}

15
bright-extension/mark.tsx Normal file
View File

@ -0,0 +1,15 @@
import { Extension } from "bright";
export const mark: Extension = {
name: "mark",
InlineAnnotation: ({ children, query }) => {
return (
<mark style={{ background: query }}>{children}</mark>
)
},
MultilineAnnotation: ({ children, query }) => {
return (
<div style={{ background: query }}>{children}</div>
)
},
};

View File

@ -1,78 +1,13 @@
import { promises as fsp } from 'fs'
export async function DummyPostString() {
const ReactDOMServer = (await import('react-dom/server')).default
const component = await DummyPost()
return ReactDOMServer.renderToStaticMarkup(component)
let path = ""
if ('DUMMY_HTML_DIR' in process.env && typeof process.env.DUMMY_HTML_DIR === "string") {
path = process.env.DUMMY_HTML_DIR + "test1.html";
}
return await fsp.readFile(path, "utf-8")
}
export async function DummyPostSlug() {
return "dummy-post"
}
export async function DummyPost() {
return(
<div>
<h1 className="title">Nginx + SSL Client Certificate Verification: Manage access to a site</h1>
<p className="paragraph">Access control is a fundamental part of security. Most entities rely on
the combination of username and password, sometimes with additional multi-factor authentication
to improve security. Some entities also use the SSL client certificate verification to manage access
to specific resources. One of the use cases where SSL client certificate verification fits perfectly is
managing access to internet-facing development or staging servers. In this post, I&apos;ll share how
to set up the certificates and configure nginx to verify users based on their certificates.</p>
<h1>Preparing the certificates</h1>
<p className="paragraph">There are two certificates we are going to create. The first one is the root
certificate. It will be placed in the Nginx server. The second one is the client certificate. It will
be installed in the client machine/browsers.</p>
<h2>Root CA</h2>
<p className="paragraph">For generating a root CA, execute these two steps:</p>
<h3>Generate RSA Key</h3>
<p className="code">openssl genrsa -aes256 -out ca.key 4096</p>
<h3>Create Root CA crt file.</h3>
<p className="code">openssl req -new -x509 -days 3650 -key ca.key -out ca.crt</p>
<h2>Setup CA configuration</h2>
<p className="paragraph">This is an optional step, but if you want to be able to revoke access you
previously granted, you need to do this step.</p>
<p className="paragraph">Create a file named ca.cnf in the same directory as the ca.key and ca.crt.</p>
<p className="code">[ ca ]
default_ca = gca
[ crl_ext ]
authorityKeyIdentifier=keyid:always
[ gca ]
dir = ./
new_certs_dir = $dir
unique_subject = no
certificate = $dir/ca.crt
database = $dir/certindex
private_key = $dir/ca.key
serial = $dir/certserial
default_days = 365
default_md = sha256
policy = gca_policy
x509_extensions = gca_extensions
crlnumber = $dir/crlnumber
default_crl_days = 365
[ gca_policy ]
commonName = supplied
stateOrProvinceName = supplied
countryName = optional
emailAddress = optional
organizationName = supplied
organizationUnitName = optional
[ gca_extensions ]
basicConstraints = CA:false
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
keyUsage = digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
crlDistributionPoints = URI:http://example.com/root.crl
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = example.com
DNS.2 = *.example.com</p>
</div>
)
}

130
dummies/test1.html Normal file
View File

@ -0,0 +1,130 @@
<h1 class="title">Nginx + SSL Client Certificate Verification: Manage access to a site</h1>
<p class="paragraph">Access control is a fundamental part of security. Most entities rely on
the combination of username and password, sometimes with additional multi-factor authentication
to improve security. Some entities also use the SSL client certificate verification to manage access
to specific resources. One of the use cases where SSL client certificate verification fits perfectly is
managing access to internet-facing development or staging servers. In this post, I&apos;ll share how
to set up the certificates and configure nginx to verify users based on their certificates.</p>
<h1>Preparing the certificates</h1>
<p class="paragraph">There are two certificates we are going to create. The first one is the root
certificate. It will be placed in the Nginx server. The second one is the client certificate. It will
be installed in the client machine/browsers.</p>
<h2>Root CA</h2>
<p class="paragraph">For generating a root CA, execute these two steps:</p>
<h3>Generate RSA Key</h3>
<code lang="shell">openssl genrsa -aes256 -out ca.key 4096</code>
<h3>Create Root CA crt file.</h3>
<code lang="shell">openssl req -new -x509 -days 3650 -key ca.key -out ca.crt</code>
<h2>Setup CA configuration</h2>
<p class="paragraph">This is an optional step, but if you want to be able to revoke access you
previously granted, you need to do this step.</p>
<p class="paragraph">Create a file named ca.cnf in the same directory as the ca.key and ca.crt.</p>
<code lang="ini" class="linenumber">[ ca ]
default_ca = gca
[ crl_ext ]
authorityKeyIdentifier=keyid:always
[ gca ]
dir = ./
new_certs_dir = $dir
unique_subject = no
certificate = $dir/ca.crt
database = $dir/certindex
; mark(1[9:11]) dimgrey
private_key = $dir/ca.key
serial = $dir/certserial
default_days = 365
default_md = sha256
policy = gca_policy
x509_extensions = gca_extensions
crlnumber = $dir/crlnumber
default_crl_days = 365
[ gca_policy ]
commonName = supplied
stateOrProvinceName = supplied
countryName = optional
emailAddress = optional
organizationName = supplied
organizationUnitName = optional
[ gca_extensions ]
basicConstraints = CA:false
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
keyUsage = digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
crlDistributionPoints = URI:http://example.com/root.crl
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = example.com
DNS.2 = *.example.com</code>
<p class="paragraph">Initialize an empty file for the CA database.</p>
<code lang="shell">touch certindex</code>
<p class="paragraph">Initialize value for certserial and crlnumber</p>
<code lang="shell">echo 01 > certserial
echo 01 > crlnumber</code>
<h2>User Certificates</h2>
<h3>Generate the user RSA key.</h3>
<code lang="shell">openssl genrsa -aes256 -out client01/user.key 4096</code>
<h3>Create Certificate-Signing Request (CSR)</h3>
<code lang="shell">openssl req -new -key client01/user.key -out client01/user.csr</code>
<h3>Sign the CSR.</h3>
<p class="paragraph">If you did the setup CA configuration step, sign the CSR file by running this command.</p>
<code lang="shell">openssl ca -config ca.cnf -in client01/user.csr -out client01/user.crt</code>
<p class="paragraph">If you skipped the setup CA configuration step, sign the CSR file by running this command.</p>
<code lang="shell">openssl x509 -req -days 365 -in client01/user.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client01/user.crt</code>
<h3>Convert the crt file to pfx/p12 file.</h3>
<p class="paragraph">Most of the time, browsers/client machines only accept a certificate in the pfx format. Run this
command to convert the crt file to the pfx/p12 format.</p>
<code lang="shell">openssl pkcs12 -export -out client01/user.pfx -inkey client01/user.key -in client01/user.crt -certfile ca.crt</code>
<p class="paragraph">You'll be prompted to enter an export password. You must input the exact password when adding
the certificate to a browser.</p>
<br/>
<h1>Setting up nginx with client certificates verification</h1>
<p class="paragraph">Add these lines to a server block in your nginx configuration</p>
<code lang="shell" class="linenumber">ssl_client_certificate /path/to/client/verfication/ca.crt;
ssl_verify_client optional;
ssl_verify_depth 2;</code>
<p class="paragraph">You can do location-based access control. Location-based here refers to a location block in your
nginx configuration, for example:</p>
<code lang="shell"> location /private {
# mark(1[13:41]) dimgrey
if ($ssl_client_verify != SUCCESS) {
return 403;
}
....
}
</code>
<p class="paragraph">Here is a complete example of a server block in the nginx configuration</p>
<code lang="shell">server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name www.example.com;
ssl_certificate /path/to/your/https/certificate.pem;
ssl_certificate_key /path/to/your/https/private-key.pem;
include snippets/ssl-params.conf;
# mark(1:3) dimgrey
ssl_client_certificate /path/to/client/verification/ca.crt;
ssl_verify_client optional;
ssl_verify_depth 2;
root /usr/share/nginx/html;
index index.html index.htm;
location / {
# mark(1[13:41]) dimgrey
if ($ssl_client_verify != SUCCESS) {
return 403;
}
}
}</code>
<br/>
<h1>Adding the User Certificates to the client machine/browsers</h1>

View File

@ -15,6 +15,7 @@
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.16",
"bright": "^0.8.4",
"dompurify": "^3.0.6",
"eslint": "8.50.0",
"eslint-config-next": "13.5.2",

View File

@ -23,6 +23,9 @@ dependencies:
autoprefixer:
specifier: 10.4.16
version: 10.4.16(postcss@8.4.30)
bright:
specifier: ^0.8.4
version: 0.8.4(react@18.2.0)
dompurify:
specifier: ^3.0.6
version: 3.0.6
@ -87,6 +90,10 @@ packages:
regenerator-runtime: 0.14.0
dev: false
/@code-hike/lighter@0.8.1:
resolution: {integrity: sha512-St4rPmB7C2EWmAK1sAbvD3lZeM7UDInVDMjQDzEDsu4Q3B3AqF25vXedQK51U0UO0MCOASgBBdTiNwvJAfIqMQ==}
dev: false
/@eslint-community/eslint-utils@4.4.0(eslint@8.50.0):
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@ -685,6 +692,16 @@ packages:
fill-range: 7.0.1
dev: false
/bright@0.8.4(react@18.2.0):
resolution: {integrity: sha512-SBlcJje8EDh3lLihqr0W27yHCVpuh3NReziNxZOwUzuaShqq+V/imY+J+AvJdTIOifiCQkyIsgVWKAj5G/eOyg==}
peerDependencies:
react: ^18
dependencies:
'@code-hike/lighter': 0.8.1
react: 18.2.0
server-only: 0.0.1
dev: false
/browserslist@4.21.11:
resolution: {integrity: sha512-xn1UXOKUz7DjdGlg9RrUr0GGiWzI97UQJnugHtH0OLDfJB7jMgoIkYvRIEO1l9EeEERVqeqLYOcFBW9ldjypbQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@ -2664,6 +2681,10 @@ packages:
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
dev: false
/server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
dev: false
/set-function-name@2.0.1:
resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==}
engines: {node: '>= 0.4'}