UNPKG

newsie

Version:

An NNTP Client Library targeting NodeJS. It supports the authentication, TLS encryption, base NNTP commands, and more.

506 lines (452 loc) 16.6 kB
import * as tls from 'tls' import { c, client, integrationSetup, s, server } from './IntegrationCommon' integrationSetup() describe('2.1. Advertising the AUTHINFO Extension', () => { test(`Example of AUTHINFO capabilities before and after the use of the STARTTLS [NNTP-TLS] extension:`, async () => { c('CAPABILITIES') s('101 Capability list:') s('VERSION 2') s('READER') s('IHAVE') s('STARTTLS') s('AUTHINFO SASL') s('SASL CRAM-MD5 DIGEST-MD5 GSSAPI') s('LIST ACTIVE NEWSGROUPS') s('.') let response = await client.capabilities() expect(response).toEqual({ code: 101, comment: 'Capability list:', description: 'Capability list follows (multi-line)', capabilities: { VERSION: ['2'], READER: [], IHAVE: [], STARTTLS: [], AUTHINFO: ['SASL'], SASL: ['CRAM-MD5', 'DIGEST-MD5', 'GSSAPI'], LIST: ['ACTIVE', 'NEWSGROUPS'] } }) c('STARTTLS') s('382 Continue with TLS negotiation', server.upgradeTls) // [TLS negotiation proceeds, further commands protected by TLS] response = await client.startTls() expect(response).toEqual({ code: 382, comment: 'Continue with TLS negotiation', description: 'Continue with TLS negotiation', socket: expect.any(tls.TLSSocket) }) c('CAPABILITIES') s('101 Capability list:') s('VERSION 2') s('READER') s('IHAVE') s('AUTHINFO USER SASL') s('SASL CRAM-MD5 DIGEST-MD5 GSSAPI PLAIN EXTERNAL') s('LIST ACTIVE NEWSGROUPS') s('.') response = await client.capabilities() expect(response).toEqual({ code: 101, comment: 'Capability list:', description: 'Capability list follows (multi-line)', capabilities: { VERSION: ['2'], READER: [], IHAVE: [], AUTHINFO: ['USER', 'SASL'], SASL: ['CRAM-MD5', 'DIGEST-MD5', 'GSSAPI', 'PLAIN', 'EXTERNAL'], LIST: ['ACTIVE', 'NEWSGROUPS'] } }) }) }) describe('2.3. AUTHINFO USER/PASS Command', () => { test('Example of successful AUTHINFO USER:', async () => { c('AUTHINFO USER wilma') s('281 Authentication accepted') const response = await client.authInfoUser('wilma') expect(response).toEqual({ code: 281, comment: 'Authentication accepted', description: 'Authentication accepted' }) }) test('Example of successful AUTHINFO USER/PASS:', async () => { c('AUTHINFO USER fred') s('381 Enter passphrase') let response = await client.authInfoUser('fred') expect(response).toEqual({ code: 381, comment: 'Enter passphrase', description: 'Password required', authInfoPass: expect.any(Function) }) c('AUTHINFO PASS flintstone') s('281 Authentication accepted') response = await response.authInfoPass('flintstone') expect(response).toEqual({ code: 281, comment: 'Authentication accepted', description: 'Authentication accepted' }) }) test('Example of AUTHINFO USER/PASS requiring a security layer:', async () => { c('AUTHINFO USER fred@stonecanyon.example.com') s('483 Encryption or stronger authentication required') let caught = false return client .authInfoUser('fred@stonecanyon.example.com') .catch(response => { caught = true expect(response).toEqual({ code: 483, comment: 'Encryption or stronger authentication required', description: 'command unavailable until suitable privacy has been arranged' }) }) .then(() => expect(caught).toBe(true)) }) test('Example of failed AUTHINFO USER/PASS:', async () => { c('AUTHINFO USER barney') s('381 Enter passphrase') const response = await client.authInfoUser('barney') expect(response).toEqual({ code: 381, comment: 'Enter passphrase', description: 'Password required', authInfoPass: expect.any(Function) }) c('AUTHINFO PASS flintstone') s('481 Authentication failed') let caught = false return response .authInfoPass('flintstone') .catch(response => { caught = true expect(response).toEqual({ code: 481, comment: 'Authentication failed', description: 'Authentication failed/rejected' }) }) .then(() => expect(caught).toBe(true)) }) test('Example of AUTHINFO PASS before AUTHINFO USER:', async () => { c('AUTHINFO PASS flintstone') s('482 Authentication commands issued out of sequence') let caught = false return client .command('AUTHINFO PASS', 'flintstone') .catch(response => { caught = true expect(response).toEqual({ code: 482, comment: 'Authentication commands issued out of sequence', description: 'Authentication commands issued out of sequence' }) }) .then(() => expect(caught).toBe(true)) }) }) describe('2.4. AUTHINFO SASL Command', () => { test(`Example of the [PLAIN] SASL mechanism under a TLS layer, using an initial client response:`, async () => { c('CAPABILITIES') s('101 Capability list:') s('VERSION 2') s('READER') s('STARTTLS') s('AUTHINFO SASL') s('SASL CRAM-MD5 DIGEST-MD5 GSSAPI') s('LIST ACTIVE NEWSGROUPS') s('.') let response = await client.capabilities() expect(response).toEqual({ code: 101, comment: 'Capability list:', description: 'Capability list follows (multi-line)', capabilities: { VERSION: ['2'], READER: [], STARTTLS: [], AUTHINFO: ['SASL'], SASL: ['CRAM-MD5', 'DIGEST-MD5', 'GSSAPI'], LIST: ['ACTIVE', 'NEWSGROUPS'] } }) c('STARTTLS') s('382 Continue with TLS negotiation', server.upgradeTls) // [TLS negotiation proceeds, further commands protected by TLS] response = await client.startTls() expect(response).toEqual({ code: 382, comment: 'Continue with TLS negotiation', description: 'Continue with TLS negotiation', socket: expect.any(tls.TLSSocket) }) c('CAPABILITIES') s('101 Capability list:') s('VERSION 2') s('READER') s('AUTHINFO USER SASL') s('SASL CRAM-MD5 DIGEST-MD5 GSSAPI PLAIN EXTERNAL') s('LIST ACTIVE NEWSGROUPS') s('.') response = await client.capabilities() expect(response).toEqual({ code: 101, comment: 'Capability list:', description: 'Capability list follows (multi-line)', capabilities: { VERSION: ['2'], READER: [], AUTHINFO: ['USER', 'SASL'], SASL: ['CRAM-MD5', 'DIGEST-MD5', 'GSSAPI', 'PLAIN', 'EXTERNAL'], LIST: ['ACTIVE', 'NEWSGROUPS'] } }) c('AUTHINFO SASL PLAIN AHRlc3QAMTIzNA==') s('281 Authentication accepted') response = await client.authInfoSaslPlain(undefined, 'test', '1234') expect(response).toEqual({ code: 281, description: 'Authentication accepted', comment: 'Authentication accepted' }) }) test(`Example of the EXTERNAL SASL mechanism under a TLS layer, using the authorization identity derived from the client TLS certificate, and thus a zero-length initial client response (commands prior to AUTHINFO SASL are the same as the previous example and have been omitted):`, async () => { c('AUTHINFO SASL EXTERNAL =') s('281 Authentication accepted') const response = await client.authInfoSasl('EXTERNAL', '=') expect(response).toEqual({ code: 281, comment: 'Authentication accepted', description: 'Authentication accepted' }) }) test(`Example of the [DIGEST-MD5] SASL mechanism, which includes a server challenge and server success data (white space has been inserted for clarity; base64-encoded data is actually sent as a single line with no embedded white space):`, async () => { c('AUTHINFO SASL DIGEST-MD5') s( '383 bm9uY2U9InNheUFPaENFS0dJZFBNSEMwd3RsZUxxT0ljT0kyd1FZSWU0' + 'enplQXR1aVE9IixyZWFsbT0iZWFnbGUub2NlYW5hLmNvbSIscW9wPSJhdXRo' + 'LGF1dGgtaW50LGF1dGgtY29uZiIsY2lwaGVyPSJyYzQtNDAscmM0LTU2LHJj' + 'NCxkZXMsM2RlcyIsbWF4YnVmPTQwOTYsY2hhcnNldD11dGYtOCxhbGdvcml0' + 'aG09bWQ1LXNlc3M=' ) let response = await client.authInfoSasl('DIGEST-MD5') expect(response).toEqual({ code: 383, comment: '', description: 'Continue with SASL exchange', cancel: expect.any(Function), continue: expect.any(Function), challenge: 'bm9uY2U9InNheUFPaENFS0dJZFBNSEMwd3RsZUxxT0ljT0kyd1FZSWU0' + 'enplQXR1aVE9IixyZWFsbT0iZWFnbGUub2NlYW5hLmNvbSIscW9wPSJhdXRo' + 'LGF1dGgtaW50LGF1dGgtY29uZiIsY2lwaGVyPSJyYzQtNDAscmM0LTU2LHJj' + 'NCxkZXMsM2RlcyIsbWF4YnVmPTQwOTYsY2hhcnNldD11dGYtOCxhbGdvcml0' + 'aG09bWQ1LXNlc3M=' }) c( 'dXNlcm5hbWU9InRlc3QiLHJlYWxtPSJlYWdsZS5vY2VhbmEuY29tIixub25j' + 'ZT0ic2F5QU9oQ0VLR0lkUE1IQzB3dGxlTHFPSWNPSTJ3UVlJZTR6emVBdHVp' + 'UT0iLGNub25jZT0iMFkzSlFWMlRnOVNjRGlwK08xU1ZDMHJoVmcvLytkbk9J' + 'aUd6LzdDZU5KOD0iLG5jPTAwMDAwMDAxLHFvcD1hdXRoLWNvbmYsY2lwaGVy' + 'PXJjNCxtYXhidWY9MTAyNCxkaWdlc3QtdXJpPSJubnRwL2xvY2FsaG9zdCIs' + 'cmVzcG9uc2U9ZDQzY2Y2NmNmZmE5MDNmOWViMDM1NmMwOGEzZGIwZjI=' ) s('283 cnNwYXV0aD1kZTJlMTI3ZTVhODFjZGE1M2Q5N2FjZGEzNWNkZTgzYQ==') response = await response.continue( 'dXNlcm5hbWU9InRlc3QiLHJlYWxtPSJlYWdsZS5vY2VhbmEuY29tIixub25j' + 'ZT0ic2F5QU9oQ0VLR0lkUE1IQzB3dGxlTHFPSWNPSTJ3UVlJZTR6emVBdHVp' + 'UT0iLGNub25jZT0iMFkzSlFWMlRnOVNjRGlwK08xU1ZDMHJoVmcvLytkbk9J' + 'aUd6LzdDZU5KOD0iLG5jPTAwMDAwMDAxLHFvcD1hdXRoLWNvbmYsY2lwaGVy' + 'PXJjNCxtYXhidWY9MTAyNCxkaWdlc3QtdXJpPSJubnRwL2xvY2FsaG9zdCIs' + 'cmVzcG9uc2U9ZDQzY2Y2NmNmZmE5MDNmOWViMDM1NmMwOGEzZGIwZjI=' ) expect(response).toEqual({ code: 283, comment: '', description: 'Authentication accepted (with success data)', challenge: 'cnNwYXV0aD1kZTJlMTI3ZTVhODFjZGE1M2Q5N2FjZGEzNWNkZTgzYQ==' }) }) test(`Example of a failed authentication due to bad [GSSAPI] credentials. Note that although the mechanism can utilize the initial response, the client chooses not to use it because of its length, resulting in a zero-length server challenge (here, white space has been inserted for clarity; base64-encoded data is actually sent as a single line with no embedded white space):`, async () => { c('AUTHINFO SASL GSSAPI') s('383 =') const response = await client.authInfoSasl('GSSAPI') expect(response).toEqual({ code: 383, comment: '', description: 'Continue with SASL exchange', cancel: expect.any(Function), continue: expect.any(Function), challenge: '=' }) c( 'YIICOAYJKoZIhvcSAQICAQBuggInMIICI6ADAgEFoQMCAQ6iBwMFACAAAACj' + 'ggE/YYIBOzCCATegAwIBBaEYGxZURVNULk5FVC5JU0MuVVBFTk4uRURVoiQw' + 'IqADAgEDoRswGRsEbmV3cxsRbmV0bmV3cy51cGVubi5lZHWjge8wgeygAwIB' + 'EKEDAgECooHfBIHcSQfLKC8vm2i17EXmomwk6hHvjBY/BnKnvvDTrbno3198' + 'vlX2RSUt+CjuAKhcDcj4DW0gvZEqH7t5v9yWedzztlpaThebBat6hQNr9NJP' + 'ozh1/+74HUwhGWb50KtjuftO/ftQ8q0nTuYKgIq6PM4tp2ddo1IfpjfdNR9E' + '95GFi3y1uBT7lQOwtQbRJUjPSO3ijdue9V7cNNVmYsBsqNsaHhvlBJEXf4WJ' + 'djH8yG+Dw/gX8fUTtC5fDpB5zLt01mkSXh6Wc4UhqQtwZBI2t/+TpX1okbg6' + 'Hr1ZZupeH6SByjCBx6ADAgEQooG/BIG8GnCmcXWtqhXh48dGTLHQgJ04K5Fj' + 'RMMq2qPSbiha9lq0osqR2KAnQA6LioWYxU+6yPKpBDSC5WOT441fUfkM8iAL' + 'kW3uNc+luFCGcnDsacrmoVU7Y6Akcp9m7Fm7orRc+TWSWPpBg3OR2oG3ATW0' + '0NAz8TT06VOLVxIMUTINKdYVI/Ja7f3sy+/N4LGkJqScCQOwlo5tfDWn/UQF' + 'iTWo5Zw435rH8pjy2smQCnqC14v3NMAWTu4j+dzHUNw=' ) s('481 Authentication error') let caught = false return response .continue( 'YIICOAYJKoZIhvcSAQICAQBuggInMIICI6ADAgEFoQMCAQ6iBwMFACAAAACj' + 'ggE/YYIBOzCCATegAwIBBaEYGxZURVNULk5FVC5JU0MuVVBFTk4uRURVoiQw' + 'IqADAgEDoRswGRsEbmV3cxsRbmV0bmV3cy51cGVubi5lZHWjge8wgeygAwIB' + 'EKEDAgECooHfBIHcSQfLKC8vm2i17EXmomwk6hHvjBY/BnKnvvDTrbno3198' + 'vlX2RSUt+CjuAKhcDcj4DW0gvZEqH7t5v9yWedzztlpaThebBat6hQNr9NJP' + 'ozh1/+74HUwhGWb50KtjuftO/ftQ8q0nTuYKgIq6PM4tp2ddo1IfpjfdNR9E' + '95GFi3y1uBT7lQOwtQbRJUjPSO3ijdue9V7cNNVmYsBsqNsaHhvlBJEXf4WJ' + 'djH8yG+Dw/gX8fUTtC5fDpB5zLt01mkSXh6Wc4UhqQtwZBI2t/+TpX1okbg6' + 'Hr1ZZupeH6SByjCBx6ADAgEQooG/BIG8GnCmcXWtqhXh48dGTLHQgJ04K5Fj' + 'RMMq2qPSbiha9lq0osqR2KAnQA6LioWYxU+6yPKpBDSC5WOT441fUfkM8iAL' + 'kW3uNc+luFCGcnDsacrmoVU7Y6Akcp9m7Fm7orRc+TWSWPpBg3OR2oG3ATW0' + '0NAz8TT06VOLVxIMUTINKdYVI/Ja7f3sy+/N4LGkJqScCQOwlo5tfDWn/UQF' + 'iTWo5Zw435rH8pjy2smQCnqC14v3NMAWTu4j+dzHUNw=' ) .catch(response => { caught = true expect(response).toEqual({ code: 481, comment: 'Authentication error', description: 'Authentication failed/rejected' }) }) .then(() => expect(caught).toBe(true)) }) test('Example of a client aborting in the midst of an exchange:', async () => { c('AUTHINFO SASL GSSAPI') s('383 =') const response = await client.authInfoSasl('GSSAPI') expect(response).toEqual({ code: 383, comment: '', description: 'Continue with SASL exchange', cancel: expect.any(Function), continue: expect.any(Function), challenge: '=' }) c('*') s('481 Authentication aborted as requested') let caught = false return response .cancel() .catch(response => { caught = true expect(response).toEqual({ code: 481, comment: 'Authentication aborted as requested', description: 'Authentication failed/rejected' }) }) .then(() => expect(caught).toBe(true)) }) test(`Example of attempting to use a mechanism that is not supported by the server:`, async () => { c('AUTHINFO SASL EXAMPLE') s('503 Mechanism not recognized') let caught = false return client .authInfoSasl('EXAMPLE') .catch(response => { caught = true expect(response).toEqual({ code: 503, comment: 'Mechanism not recognized', description: 'feature not supported' }) }) .then(() => expect(caught).toBe(true)) }) test(`Example of attempting to use a mechanism that requires a security layer:`, async () => { c('AUTHINFO SASL PLAIN') s('483 Encryption or stronger authentication required') let caught = false return client .authInfoSasl('PLAIN') .catch(response => { caught = true expect(response).toEqual({ code: 483, comment: 'Encryption or stronger authentication required', description: 'command unavailable until suitable privacy has been arranged' }) }) .then(() => expect(caught).toBe(true)) }) test(`Example of using an initial response with a mechanism that doesn't support it (the server must start the exchange when using [CRAM-MD5]):`, async () => { c('AUTHINFO SASL CRAM-MD5 AHRlc3QAMTIzNA==') s('482 SASL protocol error') let caught = false return client .authInfoSasl('CRAM-MD5', 'AHRlc3QAMTIzNA==') .catch(response => { caught = true expect(response).toEqual({ code: 482, comment: 'SASL protocol error', description: 'SASL protocol error' }) }) .then(() => expect(caught).toBe(true)) }) test(`Example of an authentication that failed due to an incorrectly encoded response:`, async () => { c('AUTHINFO SASL CRAM-MD5') s('383 PDE1NDE2NzQ5My4zMjY4MzE3QHRlc3RAZXhhbXBsZS5jb20+') const response = await client.authInfoSasl('CRAM-MD5') expect(response).toEqual({ code: 383, comment: '', description: 'Continue with SASL exchange', cancel: expect.any(Function), continue: expect.any(Function), challenge: 'PDE1NDE2NzQ5My4zMjY4MzE3QHRlc3RAZXhhbXBsZS5jb20+' }) c('abcd=efg') s('504 Base64 encoding error') let caught = false return response .continue('abcd=efg') .catch(response => { caught = true expect(response).toEqual({ code: 504, comment: 'Base64 encoding error', description: 'error in base64-encoding [RFC4648] of an argument' }) }) .then(() => expect(caught).toBe(true)) }) })