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
text/typescript
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))
})
})