javascript-solid-server 0.0.73 → 0.0.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +29 -1
- package/README.md +63 -3
- package/bin/jss.js +7 -0
- package/package.json +1 -1
- package/src/auth/middleware.js +6 -3
- package/src/auth/solid-oidc.js +2 -2
- package/src/auth/token.js +45 -30
- package/src/auth/webid-tls.js +270 -0
- package/src/config.js +9 -0
- package/src/handlers/resource.js +104 -6
- package/src/ldp/headers.js +3 -2
- package/src/mashlib/index.js +107 -0
- package/src/notifications/index.js +5 -2
- package/src/notifications/websocket.js +63 -3
- package/src/server.js +47 -1
- package/src/storage/filesystem.js +22 -0
- package/test/helpers.js +2 -0
- package/test/notifications.test.js +90 -0
- package/test/range.test.js +145 -0
- package/test/webid-tls.test.js +119 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Range Request Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests HTTP Range header support for partial content delivery.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, before, after } from 'node:test';
|
|
8
|
+
import assert from 'node:assert';
|
|
9
|
+
import {
|
|
10
|
+
startTestServer,
|
|
11
|
+
stopTestServer,
|
|
12
|
+
request,
|
|
13
|
+
createTestPod,
|
|
14
|
+
assertStatus
|
|
15
|
+
} from './helpers.js';
|
|
16
|
+
|
|
17
|
+
describe('Range Requests', () => {
|
|
18
|
+
const testContent = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; // 36 bytes
|
|
19
|
+
|
|
20
|
+
before(async () => {
|
|
21
|
+
await startTestServer();
|
|
22
|
+
await createTestPod('rangetest');
|
|
23
|
+
|
|
24
|
+
// Create a test file with known content
|
|
25
|
+
await request('/rangetest/public/test.txt', {
|
|
26
|
+
method: 'PUT',
|
|
27
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
28
|
+
body: testContent,
|
|
29
|
+
auth: 'rangetest'
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
after(async () => {
|
|
34
|
+
await stopTestServer();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('Accept-Ranges header', () => {
|
|
38
|
+
it('should include Accept-Ranges: bytes for files', async () => {
|
|
39
|
+
const res = await request('/rangetest/public/test.txt');
|
|
40
|
+
assertStatus(res, 200);
|
|
41
|
+
assert.strictEqual(res.headers.get('Accept-Ranges'), 'bytes');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should include Accept-Ranges: none for containers', async () => {
|
|
45
|
+
const res = await request('/rangetest/public/');
|
|
46
|
+
assertStatus(res, 200);
|
|
47
|
+
assert.strictEqual(res.headers.get('Accept-Ranges'), 'none');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('Range header parsing', () => {
|
|
52
|
+
it('should return 206 for valid range bytes=0-9', async () => {
|
|
53
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
54
|
+
headers: { 'Range': 'bytes=0-9' }
|
|
55
|
+
});
|
|
56
|
+
assertStatus(res, 206);
|
|
57
|
+
|
|
58
|
+
const body = await res.text();
|
|
59
|
+
assert.strictEqual(body, 'ABCDEFGHIJ');
|
|
60
|
+
assert.strictEqual(res.headers.get('Content-Range'), 'bytes 0-9/36');
|
|
61
|
+
assert.strictEqual(res.headers.get('Content-Length'), '10');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return 206 for open-ended range bytes=30-', async () => {
|
|
65
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
66
|
+
headers: { 'Range': 'bytes=30-' }
|
|
67
|
+
});
|
|
68
|
+
assertStatus(res, 206);
|
|
69
|
+
|
|
70
|
+
const body = await res.text();
|
|
71
|
+
assert.strictEqual(body, '456789');
|
|
72
|
+
assert.strictEqual(res.headers.get('Content-Range'), 'bytes 30-35/36');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should return 206 for suffix range bytes=-6', async () => {
|
|
76
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
77
|
+
headers: { 'Range': 'bytes=-6' }
|
|
78
|
+
});
|
|
79
|
+
assertStatus(res, 206);
|
|
80
|
+
|
|
81
|
+
const body = await res.text();
|
|
82
|
+
assert.strictEqual(body, '456789');
|
|
83
|
+
assert.strictEqual(res.headers.get('Content-Range'), 'bytes 30-35/36');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should clamp end to file size for range exceeding file', async () => {
|
|
87
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
88
|
+
headers: { 'Range': 'bytes=30-1000' }
|
|
89
|
+
});
|
|
90
|
+
assertStatus(res, 206);
|
|
91
|
+
|
|
92
|
+
const body = await res.text();
|
|
93
|
+
assert.strictEqual(body, '456789');
|
|
94
|
+
assert.strictEqual(res.headers.get('Content-Range'), 'bytes 30-35/36');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('Multi-range requests', () => {
|
|
99
|
+
it('should ignore multi-range and return 200 with full content', async () => {
|
|
100
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
101
|
+
headers: { 'Range': 'bytes=0-5,10-15' }
|
|
102
|
+
});
|
|
103
|
+
// Multi-range is not supported, should fall back to 200
|
|
104
|
+
assertStatus(res, 200);
|
|
105
|
+
|
|
106
|
+
const body = await res.text();
|
|
107
|
+
assert.strictEqual(body, testContent);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('Invalid ranges', () => {
|
|
112
|
+
it('should return 200 for invalid range format', async () => {
|
|
113
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
114
|
+
headers: { 'Range': 'invalid' }
|
|
115
|
+
});
|
|
116
|
+
// Invalid format, ignore Range header
|
|
117
|
+
assertStatus(res, 200);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should return 200 for non-bytes range unit', async () => {
|
|
121
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
122
|
+
headers: { 'Range': 'chars=0-10' }
|
|
123
|
+
});
|
|
124
|
+
assertStatus(res, 200);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('RDF resources', () => {
|
|
129
|
+
it('should ignore Range header for RDF resources', async () => {
|
|
130
|
+
// Create an RDF resource
|
|
131
|
+
await request('/rangetest/public/data.jsonld', {
|
|
132
|
+
method: 'PUT',
|
|
133
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
134
|
+
body: JSON.stringify({ '@id': '#test', 'http://example.org/name': 'Test' }),
|
|
135
|
+
auth: 'rangetest'
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const res = await request('/rangetest/public/data.jsonld', {
|
|
139
|
+
headers: { 'Range': 'bytes=0-10' }
|
|
140
|
+
});
|
|
141
|
+
// RDF resources don't support range requests
|
|
142
|
+
assertStatus(res, 200);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebID-TLS Authentication tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the WebID-TLS certificate parsing and verification logic.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it } from 'node:test';
|
|
8
|
+
import assert from 'node:assert';
|
|
9
|
+
import {
|
|
10
|
+
extractWebIdFromSAN,
|
|
11
|
+
verifyWebIdTls,
|
|
12
|
+
clearCache
|
|
13
|
+
} from '../src/auth/webid-tls.js';
|
|
14
|
+
|
|
15
|
+
describe('WebID-TLS', () => {
|
|
16
|
+
describe('extractWebIdFromSAN', () => {
|
|
17
|
+
it('should extract WebID from simple SAN', () => {
|
|
18
|
+
const san = 'URI:https://alice.example/card#me';
|
|
19
|
+
const webId = extractWebIdFromSAN(san);
|
|
20
|
+
assert.strictEqual(webId, 'https://alice.example/card#me');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should extract WebID from multi-value SAN', () => {
|
|
24
|
+
const san = 'URI:https://bob.example/profile/card#me, DNS:example.com, IP:192.168.1.1';
|
|
25
|
+
const webId = extractWebIdFromSAN(san);
|
|
26
|
+
assert.strictEqual(webId, 'https://bob.example/profile/card#me');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return null for missing SAN', () => {
|
|
30
|
+
const webId = extractWebIdFromSAN(null);
|
|
31
|
+
assert.strictEqual(webId, null);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return null for SAN without URI', () => {
|
|
35
|
+
const san = 'DNS:example.com, IP:192.168.1.1';
|
|
36
|
+
const webId = extractWebIdFromSAN(san);
|
|
37
|
+
assert.strictEqual(webId, null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should handle URI without spaces', () => {
|
|
41
|
+
const san = 'URI:https://user.example/me,DNS:example.com';
|
|
42
|
+
const webId = extractWebIdFromSAN(san);
|
|
43
|
+
assert.strictEqual(webId, 'https://user.example/me');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('verifyWebIdTls', () => {
|
|
48
|
+
// Clear cache before each test
|
|
49
|
+
it('should reject certificate without modulus', async () => {
|
|
50
|
+
clearCache();
|
|
51
|
+
const cert = { exponent: '10001' }; // Missing modulus
|
|
52
|
+
try {
|
|
53
|
+
await verifyWebIdTls(cert, 'https://example.com/card#me');
|
|
54
|
+
assert.fail('Should have thrown an error');
|
|
55
|
+
} catch (err) {
|
|
56
|
+
assert.ok(err.message.includes('modulus'));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should reject certificate without exponent', async () => {
|
|
61
|
+
clearCache();
|
|
62
|
+
const cert = { modulus: 'abc123' }; // Missing exponent
|
|
63
|
+
try {
|
|
64
|
+
await verifyWebIdTls(cert, 'https://example.com/card#me');
|
|
65
|
+
assert.fail('Should have thrown an error');
|
|
66
|
+
} catch (err) {
|
|
67
|
+
assert.ok(err.message.includes('exponent'));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('Certificate key extraction', () => {
|
|
73
|
+
it('should extract keys from JSON-LD profile with cert:key', async () => {
|
|
74
|
+
// This tests the internal extractCertKeys function indirectly
|
|
75
|
+
// by checking that profiles are fetched and parsed correctly
|
|
76
|
+
clearCache();
|
|
77
|
+
|
|
78
|
+
// A minimal test - full integration would need a mock server
|
|
79
|
+
// For now we just ensure the functions are callable
|
|
80
|
+
const cert = {
|
|
81
|
+
modulus: 'abc123def456',
|
|
82
|
+
exponent: '10001' // 65537 in hex
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// This will fail because the profile URL doesn't exist
|
|
87
|
+
// but it tests that the function runs without syntax errors
|
|
88
|
+
const result = await verifyWebIdTls(cert, 'https://nonexistent.example/card#me');
|
|
89
|
+
assert.strictEqual(result, false);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
// Expected to fail on network error
|
|
92
|
+
assert.ok(true);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('SAN format variations', () => {
|
|
98
|
+
it('should handle lowercase uri prefix', () => {
|
|
99
|
+
// Some certs might have lowercase
|
|
100
|
+
const san = 'uri:https://alice.example/card#me';
|
|
101
|
+
// Our regex is case-sensitive, which matches the standard
|
|
102
|
+
const webId = extractWebIdFromSAN(san);
|
|
103
|
+
assert.strictEqual(webId, null); // Should not match lowercase
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should extract first URI when multiple are present', () => {
|
|
107
|
+
const san = 'URI:https://primary.example/me, URI:https://secondary.example/me';
|
|
108
|
+
const webId = extractWebIdFromSAN(san);
|
|
109
|
+
assert.strictEqual(webId, 'https://primary.example/me');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should handle spaces in SAN format', () => {
|
|
113
|
+
const san = 'URI: https://alice.example/card#me';
|
|
114
|
+
const webId = extractWebIdFromSAN(san);
|
|
115
|
+
// With space after colon, it captures from the space
|
|
116
|
+
assert.ok(webId === null || webId === ' https://alice.example/card#me');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|