Skip to content

Commit

Permalink
#35 Rename /api route to new set of supported routes
Browse files Browse the repository at this point in the history
- `/keys`
- `/authorized_keys`

(Trailing slashes will now also yield a result).
  • Loading branch information
danielemery committed Aug 27, 2024
1 parent 9467b4b commit 7526b9d
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 33 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Simple repository to manage and distribute ssh keys.

To see a production implementation of this app feel free to visit
https://keys.demery.net/api
https://keys.demery.net/keys

Public keys are provided in a configuration file at application start. The
application has no persistence layer and is stateless.
Expand All @@ -24,7 +24,7 @@ place to prevent loss of access.
### Get all listed keys

```sh
curl "https://keys.demery.net/api"
curl "https://keys.demery.net/keys"
```

### Update authorized keys file
Expand All @@ -36,7 +36,7 @@ tag and override the `authorized_keys` file with them_
# Consider backup first
cp ~/.ssh/authorized_keys ~/.ssh/authorized_keys.`date '+%Y-%m-%d__%H_%M_%S'`.backup
# Override file with the matching keys
curl "https://keys.demery.net/api?user=demery&allOf=oak&noneOf=disabled" > ~/.ssh/authorized_keys
curl "https://keys.demery.net/keys?user=demery&allOf=oak&noneOf=disabled" > ~/.ssh/authorized_keys
# Check that they keys were updated with what you expected
cat ~/.ssh/authorized_keys
```
Expand Down
8 changes: 5 additions & 3 deletions src/filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ Deno.test("parseParameters: must parse oneOf url param", () => {
const expected: Filter = {
oneOf: ["simple"],
};
const actual = parseParameters(new URL("http://domain.com/api?oneOf=simple"));
const actual = parseParameters(
new URL("http://domain.com/keys?oneOf=simple"),
);
assertEquals(actual, expected);
});

Deno.test("parseParameters: must parse user url param", () => {
const expected: Filter = {
user: "sample",
};
const actual = parseParameters(new URL("http://domain.com/api?user=sample"));
const actual = parseParameters(new URL("http://domain.com/keys?user=sample"));
assertEquals(actual, expected);
});

Expand All @@ -26,7 +28,7 @@ Deno.test("parseParameters: must parse complex filter params", () => {
};
const actual = parseParameters(
new URL(
"http://domain.com/api?noneOf=not-me&noneOf=or-me&allOf=definitely-me&allOf=and-me&user=user-one",
"http://domain.com/keys?noneOf=not-me&noneOf=or-me&allOf=definitely-me&allOf=and-me&user=user-one",
),
);
assertEquals(actual, expected);
Expand Down
4 changes: 2 additions & 2 deletions src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Deno.test(
const parseParametersSpy = spy(parseParameters);
const filterIncludesKeySpy = spy(filterIncludesKey);

const url = `${TEST_URL}/api?oneOf=private&noneOf=public&noneOf=github`;
const url = `${TEST_URL}/keys?oneOf=private&noneOf=public&noneOf=github`;

const response = await handleRequest(new Request(url), {
parseParameters: parseParametersSpy,
Expand Down Expand Up @@ -124,7 +124,7 @@ Deno.test(
const parseParametersStub = spy(throwingParseParameters);
const filterIncludesKeyStub = spy(filterIncludesKey);

const url = `${TEST_URL}/api?oneOf=private&noneOf=public&noneOf=github`;
const url = `${TEST_URL}/keys?oneOf=private&noneOf=public&noneOf=github`;

const response = await handleRequest(new Request(url), {
parseParameters: parseParametersStub,
Expand Down
67 changes: 42 additions & 25 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface ServerDependencies {
}

/**
* Start a simple http server listening on the provided port that listens on `/api` on
* Start a simple http server listening on the provided port that listens on
* the provided port and provides authorized keys based on query string filter
* parameters.
* @param port The port to listen on.
Expand All @@ -24,19 +24,25 @@ export default function start(
dependencies: ServerDependencies,
version: string,
) {
console.log(`Server listening at :${port}/api`);
console.log(`Server listening at :${port}`);
Deno.serve({
port,
handler: (req) => handleRequest(req, dependencies, version),
});
}

const validKeysRoutes = [
"/keys",
"/keys/",
"/authorized_keys",
"/authorized_keys/",
];

export function handleRequest(
req: Request,
dependencies: ServerDependencies,
version: string,
) {
const { filterIncludesKey, parseParameters, keys } = dependencies;
try {
const url = new URL(req.url);

Expand All @@ -59,30 +65,15 @@ export function handleRequest(
});
}

/** Any other url that is not `/api` we can simply return a 404. */
if (url.pathname !== "/api") {
return new Response(undefined, {
status: STATUS_CODE.NotFound,
statusText: STATUS_TEXT[STATUS_CODE.NotFound],
});
// For each supported keys endpoint serve the keys
if (validKeysRoutes.includes(url.pathname)) {
return serveKeys(url, version, dependencies);
}

/** Parse query params into filters object and filter all public keys. */
const filter = parseParameters(url);
const filteredKeys = keys.filter((key) => filterIncludesKey(filter, key));

/** Format the public keys in a suitable way for an authorized_keys file. */
const responseData = filteredKeys
.map((key) => `${key.key} ${key.user}@${key.name}`)
.join("\n");

/** Everything worked! We're good to return the keys and OK. */
return new Response(responseData, {
status: STATUS_CODE.OK,
statusText: STATUS_TEXT[STATUS_CODE.OK],
headers: {
"X-Keys-Version": version,
},
// If the url is not recognized, return a 404.
return new Response(undefined, {
status: STATUS_CODE.NotFound,
statusText: STATUS_TEXT[STATUS_CODE.NotFound],
});
} catch (err) {
console.error(err);
Expand All @@ -92,3 +83,29 @@ export function handleRequest(
});
}
}

function serveKeys(
url: URL,
version: string,
dependencies: ServerDependencies,
) {
const { filterIncludesKey, parseParameters, keys } = dependencies;

/** Parse query params into filters object and filter all public keys. */
const filter = parseParameters(url);
const filteredKeys = keys.filter((key) => filterIncludesKey(filter, key));

/** Format the public keys in a suitable way for an authorized_keys file. */
const responseData = filteredKeys
.map((key) => `${key.key} ${key.user}@${key.name}`)
.join("\n");

/** Everything worked! We're good to return the keys and OK. */
return new Response(responseData, {
status: STATUS_CODE.OK,
statusText: STATUS_TEXT[STATUS_CODE.OK],
headers: {
"X-Keys-Version": version,
},
});
}

0 comments on commit 7526b9d

Please sign in to comment.