mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat(acme): add ACME HTTP-01 challenge routing and nginx proxy
- Refactor AcmeChallengeAPI into a BaseAPI-backed class that exposes a well-known router.
- Add CrudApiEndpoint(Route("/acme-challenge")) to AcmeChallenge model.
- Register AcmeChallengeAPI router in BaseAPIFeatureSet via (new AcmeChallengeAPI).getRouter().
- Add nginx location /.well-known to proxy ACME challenge requests to /api/acme-challenge/.well-known with proper headers, resolver and websocket support.
This commit is contained in:
@@ -615,7 +615,8 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
|
||||
const APP_NAME: string = "api";
|
||||
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, AcmeChallengeAPI);
|
||||
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, (new AcmeChallengeAPI).getRouter());
|
||||
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, OpenAPI.getRouter());
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccess
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Route from "../../Types/API/Route";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@TableAccessControl({
|
||||
@@ -24,6 +26,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
icon: IconProp.Lock,
|
||||
tableDescription: "HTTP Challege for Lets Encrypt Certificates",
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/acme-challenge"))
|
||||
@Entity({
|
||||
name: "AcmeChallenge",
|
||||
})
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import AcmeChallenge from "../../Models/DatabaseModels/AcmeChallenge";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import AcmeChallengeService, {
|
||||
Service as AcmeChallengeServiceType,
|
||||
} from "../Services/AcmeChallengeService";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
@@ -7,44 +10,57 @@ import Express, {
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import AcmeChallengeService from "../Services/AcmeChallengeService";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
export default class AcmeChallengeAPI extends BaseAPI<
|
||||
AcmeChallenge,
|
||||
AcmeChallengeServiceType
|
||||
> {
|
||||
private wellKnownRouter: ExpressRouter;
|
||||
|
||||
router.get(
|
||||
"/.well-known/acme-challenge/:token",
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
try {
|
||||
const challenge: AcmeChallenge | null =
|
||||
await AcmeChallengeService.findOneBy({
|
||||
query: {
|
||||
token: req.params["token"] as string,
|
||||
},
|
||||
select: {
|
||||
challenge: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
public constructor() {
|
||||
super(AcmeChallenge, AcmeChallengeService);
|
||||
|
||||
if (!challenge) {
|
||||
return next(new NotFoundException("Challenge not found"));
|
||||
}
|
||||
this.wellKnownRouter = Express.getRouter();
|
||||
|
||||
return Response.sendTextResponse(
|
||||
req,
|
||||
res,
|
||||
challenge.challenge as string,
|
||||
);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
this.wellKnownRouter.get(
|
||||
"/.well-known/acme-challenge/:token",
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
try {
|
||||
const challenge: AcmeChallenge | null =
|
||||
await AcmeChallengeService.findOneBy({
|
||||
query: {
|
||||
token: req.params["token"] as string,
|
||||
},
|
||||
select: {
|
||||
challenge: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default router;
|
||||
if (!challenge) {
|
||||
return next(new NotFoundException("Challenge not found"));
|
||||
}
|
||||
|
||||
return Response.sendTextResponse(
|
||||
req,
|
||||
res,
|
||||
challenge.challenge as string,
|
||||
);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public getWellKnownRouter(): ExpressRouter {
|
||||
return this.wellKnownRouter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,6 +452,23 @@ server {
|
||||
}
|
||||
}
|
||||
|
||||
# ACME Challenge for primary domain.
|
||||
location /.well-known {
|
||||
# This is for nginx not to crash when service is not available.
|
||||
resolver 127.0.0.1 valid=30s;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# enable WebSockets (for ws://sockjs not connected error in the accounts source: https://stackoverflow.com/questions/41381444/websocket-connection-failed-error-during-websocket-handshake-unexpected-respon)
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_pass http://app/api/acme-challenge/.well-known;
|
||||
}
|
||||
|
||||
# PWA manifest and service worker with proper headers for home
|
||||
location ~* ^/(manifest\.json|service-worker\.js)$ {
|
||||
resolver 127.0.0.1 valid=30s;
|
||||
|
||||
Reference in New Issue
Block a user