camomile
camomile is a Node.js HTTP proxy to route images through SSL,
compatible with unified plugins,
to safely embed user content on the web.
Contents
What is this?
This is a Node.js HTTP proxy to route images through SSL,
integrable in any Node.js server such as Express, Koa, Fastify, or Next.js.
camomile works together with rehype-github-image,
which does the following at build time:
- find all insecure HTTP image URLs in content
- generate HMAC signature of each URL
- replace the URL with a signed URL containing the encoded URL and HMAC
When a user visits your app and views the content:
- their browser requests the URLs going to your server
- camomile validates the HMAC,
decodes the URL,
requests the content from the origin server without sensitive headers,
and streams it to the client
When should I use this?
Use this when you want to embed user content on the web in a safe way.
Sometimes user content is served over HTTP,
which is not secure:
An HTTPS page that includes content fetched using cleartext HTTP is called a
mixed content page.
Pages like this are only partially encrypted,
leaving the unencrypted content accessible to sniffers and man-in-the-middle
attackers.
— MDN
This also prevents information about your users leaking to other servers.
Install
This package is ESM only.
In Node.js (version 18+),
install with npm:
npm install camomile
Use
import process from 'node:process'
import {Camomile} from 'camomile'
const secret = process.env.CAMOMILE_SECRET
if (!secret) throw new Error('Missing `CAMOMILE_SECRET` in environment')
const server = new Camomile({secret})
server.listen({host: '127.0.0.1', port: 1080})
API
This package exports the identifier
Camomile
.
It exports the TypeScript type
Options
.
There is no default export.
new Camomile(options)
Create a new camomile server with options.
Parameters
options
(Options
, required)
— configuration
Returns
Server.
Options
Configuration (TypeScript type).
Fields
maxSize
(number
, default: 100 * 1024 * 1024
)
— max size in bytes per resource to download;
a 413
is sent if the resource is larger than the maximum sizesecret
(string
, required)
— HMAC key to decrypt the URLs and used by
rehype-github-image
serverName
(string
, default: 'camomile'
)
— server name sent in Via
Examples
Example: integrate camomile into Express
import process from 'node:process'
import {Camomile} from 'camomile'
import express from 'express'
const secret = process.env.CAMOMILE_SECRET
if (!secret) throw new Error('Missing `CAMOMILE_SECRET` in environment')
const uploadApp = express()
const camomile = new Camomile({secret})
uploadApp.all('*', camomile.handle.bind(camomile))
const host = '127.0.0.1'
const port = 1080
const app = express()
app.use('/uploads', uploadApp)
app.listen(port, host)
console.log('Listening on `http://' + host + ':' + port + '/uploads/`')
Example: integrate camomile into Koa
import process from 'node:process'
import {Camomile} from 'camomile'
import Koa from 'koa'
const secret = process.env.CAMOMILE_SECRET
if (!secret) throw new Error('Missing `CAMOMILE_SECRET` in environment')
const camomile = new Camomile({secret})
const port = 1080
const app = new Koa()
app.use(function (ctx, next) {
if (/^\/files\/.+/.test(ctx.path.toLowerCase())) {
return camomile.handle(ctx.req, ctx.res)
}
return next()
})
app.listen(port)
Example: integrate camomile into Fastify
import process from 'node:process'
import {Camomile} from 'camomile'
import createFastify from 'fastify'
const secret = process.env.CAMOMILE_SECRET
if (!secret) throw new Error('Missing `CAMOMILE_SECRET` in environment')
const fastify = createFastify({logger: true})
const camomile = new Camomile({secret})
fastify.addContentTypeParser(
'application/offset+octet-stream',
function (request, payload, done) {
done(null)
}
)
fastify.all('/files', function (request, response) {
camomile.handle(request.raw, response.raw)
})
fastify.all('/files/*', function (request, response) {
camomile.handle(request.raw, response.raw)
})
fastify.listen({port: 3000}, function (error) {
if (error) {
fastify.log.error(error)
process.exit(1)
}
})
Example: integrate camomile into Next.js
Attach the camomile server handler to a Next.js route handler in an optional catch-all route file
/pages/api/upload/[[...file]].ts
import process from 'node:process'
import {Camomile} from 'camomile'
import type {NextApiRequest, NextApiResponse} from 'next'
const secret = process.env.CAMOMILE_SECRET
if (!secret) throw new Error('Missing `CAMOMILE_SECRET` in environment')
export const config = {api: {bodyParser: false}}
const camomile = new Camomile({secret})
export default async function handler(
request: NextApiRequest,
response: NextApiResponse
) {
return camomile.handle(request, response)
}
Compatibility
Projects maintained by the unified collective are compatible with maintained
versions of Node.js.
When we cut a new major release,
we drop support for unmaintained versions of Node.
This means we try to keep the current release line,
camomile@^1
,
compatible with Node.js 18.
Contribute
See contributing.md
in
rehypejs/.github
for ways
to get started.
See support.md
for ways to get help.
This project has a code of conduct.
By interacting with this repository, organization, or community you agree to
abide by its terms.
For info on how to submit a security report,
see our security policy.
Acknowledgments
In 2010 GitHub introduced camo,
a similar server in CoffeeScript,
which is now deprecated and in public archive.
This project is a spiritual successor to camo
.
A lot of inspiration was also taken from go-camo
,
which is a modern and maintained image proxy in Go.
Thanks to @kytta for the npm package name camomile
!
License
MIT © Merlijn Vos