Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions library/languages.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,21 @@ class Languages {
}
return parts.join(', ');
}

/**
* The languages as they were asked for, in the form a displayLanguage parameter takes
* (the Accept-Language syntax: "en,it;q=0.8,*"). Unlike asString, which is for reading
* in messages, there are no spaces; unlike toString, implicit languages are left out.
*/
asParameter() {
return this.languages.filter(lang => !lang.implicit).map(lang => {
if (lang.quality === undefined || lang.quality === 1.0) {
return lang.code;
} else {
return `${lang.code};q=${lang.quality}`;
}
}).join(',');
}
}

/**
Expand Down
24 changes: 16 additions & 8 deletions nginx.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ public servers (tx.fhir.org, packages.fhir.org and so on) use.

### Headers to pass to the server

FHIRsmith runs Express with `trust proxy` enabled (`server.trustProxy` in config.json,
default `true`). That means it takes the client address, scheme and host from the
`X-Forwarded-*` headers, so nginx has to set them:
FHIRsmith runs Express with `trust proxy` enabled (`server.trustProxy` in config.json; by
default it trusts proxies on the same host or a private network). That means it takes the
client address, scheme and host from the `X-Forwarded-*` headers, so nginx has to set them:

| Header | nginx value | What FHIRsmith uses it for |
|---|---|---|
Expand Down Expand Up @@ -49,11 +49,19 @@ them:

### Trusting the proxy

With `trustProxy: true`, anyone who can reach the FHIRsmith port directly can forge
`X-Forwarded-For`. FHIRsmith listens on all interfaces, so firewall the port so that only
nginx can reach it. You can also set `server.trustProxy` to `"loopback"`, or to the proxy's
address; any value that Express's `trust proxy` setting accepts will work. If you run
FHIRsmith without a proxy, set it to `false`.
By default FHIRsmith believes the `X-Forwarded-*` headers only when the request comes from
a loopback, link-local or private address (`"loopback, linklocal, uniquelocal"`) - nginx on
the same machine, or on the same docker or private network. A request straight from the
internet can't set its own client address, so it can't get round the rate limits on logins,
report submission and the publisher's task actions.

If your proxy is somewhere else (a cloud load balancer with a public address, say), set
`server.trustProxy` to its address, or to the number of proxy hops; any value Express's
`trust proxy` setting accepts will work. If you run FHIRsmith without a proxy, set it to
`false`. Avoid `true`: it believes any `X-Forwarded-For`, so anyone who can reach the
FHIRsmith port directly can name their own IP address, and express-rate-limit logs
`ERR_ERL_PERMISSIVE_TRUST_PROXY` to say so. Either way, firewalling the port so that only
nginx can reach it is still a good idea.

### Upstream connections

Expand Down
4 changes: 3 additions & 1 deletion registry/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,8 @@ class RegistryModule {
* @returns {string} Formatted URL with edition description
*/
_describeSnomedEdition(url) {
if (!url.startsWith('http://snomed.info/sct')) {
// the test edition is published under xsct (http://snomed.info/xsct/31000003106/...)
if (!url.startsWith('http://snomed.info/sct') && !url.startsWith('http://snomed.info/xsct')) {
return url;
}

Expand All @@ -745,6 +746,7 @@ class RegistryModule {
// Match edition code to description
switch (editionCode) {
case '900000000000207008': edition = 'Intl'; break;
case '31000003106': edition = 'Test'; break; // tx-ecosystem test edition (xsct)
case '731000124108': edition = 'US'; break;
case '32506021000036107': edition = 'AU'; break;
case '449081005': edition = 'ES/Intl'; break;
Expand Down
7 changes: 6 additions & 1 deletion server.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@

// Behind nginx (or any reverse proxy): honor X-Forwarded-* so req.protocol and
// req.hostname reflect what the client actually requested, not the loopback hop.
app.set('trust proxy', config.server.trustProxy ?? true);
// Only proxies on the same host or a private network are trusted by default. `true` would
// trust every X-Forwarded-For hop, so anyone who could reach the port directly could name
// their own IP address and walk around every rate limit (express-rate-limit refuses to be
// quiet about it: ERR_ERL_PERMISSIVE_TRUST_PROXY). See nginx.md.
const DEFAULT_TRUST_PROXY = 'loopback, linklocal, uniquelocal';
app.set('trust proxy', config.server.trustProxy ?? DEFAULT_TRUST_PROXY);

const PORT = process.env.PORT || config.server.port || 3000;

Expand Down Expand Up @@ -790,7 +795,7 @@
}
// Run usage tracker in background after startup
if (modules.tx && modules.tx.library) {
setImmediate(async () => {

Check warning on line 798 in server.js

View workflow job for this annotation

GitHub Actions / Code Quality

Promise returned in function argument where a void return was expected
try {
serverLog.info('Starting ConceptUsageTracker scan...');
let count = await modules.tx.usageTracker.scanValueSets(modules.tx.library);
Expand All @@ -810,7 +815,7 @@
}

// Graceful shutdown
process.on('SIGINT', async () => {

Check warning on line 818 in server.js

View workflow job for this annotation

GitHub Actions / Code Quality

Promise returned in function argument where a void return was expected
serverLog.info('\nShutting down server...');

// Shutdown all modules
Expand Down
2 changes: 1 addition & 1 deletion tests/testing/testing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function report(overrides = {}) {
// mirrors the body parsers server.js puts in front of every module
async function makeApp(config = {}) {
const app = express();
app.set('trust proxy', true);
app.set('trust proxy', 'loopback');
app.use(express.raw({ type: 'application/fhir+json', limit: '50mb' }));
app.use(express.raw({ type: 'application/fhir+xml', limit: '50mb' }));
app.use(express.json({ limit: '50mb' }));
Expand Down
75 changes: 3 additions & 72 deletions tx/cs/cs-snomed.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {ConceptMap} = require("../library/conceptmap");
const {ECLLexer, ECLParser, ECLNodeType, ECLTokenType} = require("../sct/ecl");
const {Issue} = require("../library/operation-outcome");
const {debugLog} = require("../operation-context");
const {editionName, editionCode} = require("../sct/editions");
const {SnomedTextIndex, rankMatches, filterTokens} = require("../sct/text-index");
const Logger = require("../../library/logger");

Expand Down Expand Up @@ -3038,81 +3039,11 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
}

function getEditionName(edition) {
const editionMap = {
'900000000000207008': 'International Edition',
// The terminology-ecosystem test distribution, published under xsct - see the
// tx-ecosystem IG, which requires a server claiming the snomed mode to load it.
'31000003106': 'Test Edition',
'449081005': 'International Spanish Edition',
'11000221109': 'Argentinian Edition',
'32506021000036107': 'Australian Edition (with drug extension)',
'11000234105': 'Austrian Edition',
'11000172109': 'Belgian Edition',
'20621000087109': 'Canadian English Edition',
'20611000087101': 'Canadian Canadian French Edition',
'554471000005108': 'Danish Edition',
'11000279109': 'Czech Edition',
'11000181102': 'Estonian Edition',
'11000229106': 'Finnish Edition',
'11000274103': 'German Edition',
'1121000189102': 'Indian Edition',
'827022005': 'IPS Terminology',
'11000220105': 'Irish Edition',
'11000146104': 'Netherlands Edition',
'21000210109': 'New Zealand Edition',
'51000202101': 'Norwegian Edition',
'11000267109': 'Republic of Korea Edition (South Korea)',
'900000001000122104': 'Spanish National Edition',
'45991000052106': 'Swedish Edition',
'2011000195101': 'Swiss Edition',
'83821000000107': 'UK Edition',
'999000021000000109': 'UK Clinical Edition',
'5631000179106': 'Uruguay Edition',
'731000124108': 'US Edition',
'21000325107': 'Chilean Edition',
'5991000124107': 'US Edition (with ICD-10-CM maps)'
};

return editionMap[edition] || 'Unknown Edition';
return editionName(edition) || 'Unknown Edition';
}

function getEditionCode(edition) {
const editionMap = {
'900000000000207008': 'Intl',
// The terminology-ecosystem test distribution, published under xsct - see the
// tx-ecosystem IG, which requires a server claiming the snomed mode to load it.
'31000003106': 'Test',
'449081005': 'es',
'11000221109': 'AR-es',
'32506021000036107': 'AU+',
'11000234105': 'AT',
'11000172109': 'BE',
'20621000087109': 'CA-en',
'20611000087101': 'CA-fr',
'554471000005108': 'DK',
'11000279109': 'CZ',
'11000181102': 'ES',
'11000229106': 'FI',
'11000274103': 'DE',
'1121000189102': 'IN',
'827022005': 'IPS',
'11000220105': 'IE',
'11000146104': 'NL',
'21000210109': 'NZ',
'51000202101': 'NO',
'11000267109': 'KR',
'900000001000122104': 'ES-es',
'45991000052106': 'SW',
'2011000195101': 'CH',
'83821000000107': 'UK',
'999000021000000109': 'UK-Clinical',
'5631000179106': 'UR',
'731000124108': 'US',
'21000325107': 'CL',
'5991000124107': 'US+)'
};

return editionMap[edition] || 'Unknown Edition';
return editionCode(edition) || 'Unknown Edition';
}


Expand Down
70 changes: 70 additions & 0 deletions tx/sct/editions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* The SNOMED CT editions FHIRsmith knows by name - the one table, used for the provider's
* description and name (cs-snomed.js) and for the edition named in unknown-code messages
* (SCTVersion in workers/worker.js). Keyed by the edition's module id, as it appears in a
* version URI: http://snomed.info/sct/[module]/version/[date]
*
* name - human readable edition name
* code - short code used in the provider name ("SCT US")
*
* (The importer and the registry keep their own tables: those carry import settings and
* registry codes, not display names.)
*/
const EDITIONS = {
'900000000000207008': { name: 'International Edition', code: 'Intl' },
// The terminology-ecosystem test distribution, published under xsct - see the
// tx-ecosystem IG, which requires a server claiming the snomed mode to load it.
'31000003106': { name: 'Test Edition', code: 'Test' },
'449081005': { name: 'International Spanish Edition', code: 'es' },
'11000221109': { name: 'Argentinian Edition', code: 'AR-es' },
'32506021000036107': { name: 'Australian Edition (with drug extension)', code: 'AU+' },
'11000234105': { name: 'Austrian Edition', code: 'AT' },
'11000172109': { name: 'Belgian Edition', code: 'BE' },
'20621000087109': { name: 'Canadian English Edition', code: 'CA-en' },
'20611000087101': { name: 'Canadian Canadian French Edition', code: 'CA-fr' },
'554471000005108': { name: 'Danish Edition', code: 'DK' },
'11000279109': { name: 'Czech Edition', code: 'CZ' },
'11000181102': { name: 'Estonian Edition', code: 'ET' },
'11000229106': { name: 'Finnish Edition', code: 'FI' },
'11000274103': { name: 'German Edition', code: 'DE' },
'1121000189102': { name: 'Indian Edition', code: 'IN' },
'827022005': { name: 'IPS Terminology', code: 'IPS' },
'11000220105': { name: 'Irish Edition', code: 'IE' },
'11000146104': { name: 'Netherlands Edition', code: 'NL' },
'21000210109': { name: 'New Zealand Edition', code: 'NZ' },
'51000202101': { name: 'Norwegian Edition', code: 'NO' },
'11000267109': { name: 'Republic of Korea Edition (South Korea)', code: 'KR' },
'900000001000122104': { name: 'Spanish National Edition', code: 'ES-es' },
'45991000052106': { name: 'Swedish Edition', code: 'SE' },
'2011000195101': { name: 'Swiss Edition', code: 'CH' },
'83821000000107': { name: 'UK Edition', code: 'UK' },
'999000021000000109': { name: 'UK Clinical Edition', code: 'UK-Clinical' },
'5631000179106': { name: 'Uruguay Edition', code: 'UY' },
'731000124108': { name: 'US Edition', code: 'US' },
'21000325107': { name: 'Chilean Edition', code: 'CL' },
'5991000124107': { name: 'US Edition (with ICD-10-CM maps)', code: 'US+' }
};

/**
* @param {string} edition - edition module id
* @returns {string|null} the edition's name, or null if it isn't a known edition
*/
function editionName(edition) {
const e = EDITIONS[edition];
return e ? e.name : null;
}

/**
* @param {string} edition - edition module id
* @returns {string|null} the edition's short code, or null if it isn't a known edition
*/
function editionCode(edition) {
const e = EDITIONS[edition];
return e ? e.code : null;
}

module.exports = {
EDITIONS,
editionName,
editionCode
};
4 changes: 2 additions & 2 deletions tx/workers/expand.js
Original file line number Diff line number Diff line change
Expand Up @@ -1385,9 +1385,9 @@ class ValueSetExpander {
}

if (this.params.DisplayLanguages) {
this.addParamCode(exp, 'displayLanguage', this.params.DisplayLanguages.asString(true));
this.addParamCode(exp, 'displayLanguage', this.params.DisplayLanguages.asParameter());
} else if (this.params.HTTPLanguages) {
this.addParamCode(exp, 'displayLanguage', this.params.HTTPLanguages.asString(true));
this.addParamCode(exp, 'displayLanguage', this.params.HTTPLanguages.asParameter());
}
if (this.params.designations) {
for (const s of this.params.designations) {
Expand Down
38 changes: 3 additions & 35 deletions tx/workers/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const {Issue} = require("../library/operation-outcome");
const {Languages} = require("../../library/languages");
const {ConceptMap} = require("../library/conceptmap");
const {Renderer} = require("../library/renderer");
const {editionName} = require("../sct/editions");

// The cache-id travels as an HTTP header (not an operation parameter) so proxies /
// load-balancers can act on it and the server can reject it before parsing the body.
Expand Down Expand Up @@ -1100,42 +1101,9 @@ function Unknown_Code_in_VersionSCT(url, version) {
function SCTVersion(url, ver) {
if (url !== 'http://snomed.info/sct' || !ver) {
return '';
} else {
let result = 'unknown';
let s = ver.split('/');
if (s.length >= 5) {
if (s[4] === '900000000000207008') result = 'International Edition';
else if (s[4] === '449081005') result = 'International Spanish Edition';
else if (s[4] === '11000221109') result = 'Argentinian Edition';
else if (s[4] === '32506021000036107') result = 'Australian Edition (with drug extension)';
else if (s[4] === '11000234105') result = 'Austrian Edition';
else if (s[4] === '11000172109') result = 'Belgian Edition';
else if (s[4] === '20621000087109') result = 'Canadian English Edition';
else if (s[4] === '20611000087101') result = 'Canadian Canadian French Edition';
else if (s[4] === '11000279109') result = 'Czech Edition';
else if (s[4] === '554471000005108') result = 'Danish Edition';
else if (s[4] === '11000181102') result = 'Estonian Edition';
else if (s[4] === '11000229106') result = 'Finnish Edition';
else if (s[4] === '11000274103') result = 'German Edition';
else if (s[4] === '1121000189102') result = 'Indian Edition';
else if (s[4] === '827022005') result = 'IPS Terminology';
else if (s[4] === '11000220105') result = 'Irish Edition';
else if (s[4] === '11000146104') result = 'Netherlands Edition';
else if (s[4] === '21000210109') result = 'New Zealand Edition';
else if (s[4] === '51000202101') result = 'Norwegian Edition';
else if (s[4] === '11000267109') result = 'Republic of Korea Edition (South Korea)';
else if (s[4] === '900000001000122104') result = 'Spanish National Edition';
else if (s[4] === '45991000052106') result = 'Swedish Edition';
else if (s[4] === '2011000195101') result = 'Swiss Edition';
else if (s[4] === '83821000000107') result = 'UK Edition';
else if (s[4] === '999000021000000109') result = 'UK Clinical Edition';
else if (s[4] === '5631000179106') result = 'Uruguay Edition';
else if (s[4] === '21000325107') result = 'Chilean Edition';
else if (s[4] === '731000124108') result = 'US Edition';
else if (s[4] === '5991000124107') result = 'US Edition (with ICD-10-CM maps)';
}
return result;
}
const s = ver.split('/');
return (s.length >= 5 && editionName(s[4])) || 'unknown';
}

module.exports = {
Expand Down
Loading