@dev-swarup/http-mitm-proxy 0.9.6
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/.editorconfig +17 -0
- package/.gitattributes +2 -0
- package/.github/workflows/npm-publish.yml +23 -0
- package/.github/workflows/tests.yml +63 -0
- package/README.md +555 -0
- package/bin/mitm-proxy.js +36 -0
- package/eslint.config.mjs +10 -0
- package/examples/forwardHttps.js +62 -0
- package/examples/modifyGoogle.js +44 -0
- package/examples/onCertificateMissing.js +30 -0
- package/examples/onCertificateRequired.js +23 -0
- package/examples/preventRequest.js +20 -0
- package/examples/processFullResponseBody.js +36 -0
- package/examples/removeProxyToServerContentLength.js +17 -0
- package/examples/websocket.js +31 -0
- package/examples/wildcard.js +17 -0
- package/index.d.ts +314 -0
- package/index.js +3 -0
- package/lib/ca.js +268 -0
- package/lib/middleware/gunzip.js +19 -0
- package/lib/middleware/wildcard.js +24 -0
- package/lib/proxy.js +1199 -0
- package/package.json +45 -0
- package/test/01_proxy.js +555 -0
- package/test/http.client.js +37 -0
- package/test/tunnel.agent.js +321 -0
- package/test/www/1024.bin +64 -0
- package/test/wwwA/1024.bin +64 -0
- package/test/wwwA/example.com.html +8 -0
- package/test/wwwA/index.html +0 -0
- package/test/wwwB/1024.bin +64 -0
- package/test/wwwB/index.html +0 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
const net = require('net');
|
|
2
|
+
const tls = require('tls');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const events = require('events');
|
|
6
|
+
|
|
7
|
+
const debug = require('debug')('http-mitm-proxy:tunnelagent');
|
|
8
|
+
|
|
9
|
+
let agentCount = 0;
|
|
10
|
+
|
|
11
|
+
class SocketStore {
|
|
12
|
+
sockets = {};
|
|
13
|
+
|
|
14
|
+
insert(key, socket) {
|
|
15
|
+
if (this.sockets[key]) {
|
|
16
|
+
this.sockets[key].push(socket);
|
|
17
|
+
} else {
|
|
18
|
+
this.sockets[key] = [socket];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get(key) {
|
|
23
|
+
if (this.sockets[key] && this.sockets[key].length > 0) {
|
|
24
|
+
return this.sockets[key].pop();
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
length(key) {
|
|
30
|
+
if (this.sockets[key]) {
|
|
31
|
+
return this.sockets[key].length;
|
|
32
|
+
}
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
count() {
|
|
37
|
+
let sum = 0;
|
|
38
|
+
for (const property in this.sockets) {
|
|
39
|
+
if (this.sockets.hasOwnProperty(property)) {
|
|
40
|
+
sum += this.sockets[property].length;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return sum;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
remove(key, socket) {
|
|
47
|
+
const socketIndex = this.sockets[key].indexOf(socket);
|
|
48
|
+
if (socketIndex === -1) {
|
|
49
|
+
throw new Error('SocketStore: Attempt to remove non-existing socket');
|
|
50
|
+
}
|
|
51
|
+
this.sockets[key].splice(socketIndex, 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
replace(key, socket, newSocket) {
|
|
55
|
+
this.sockets[key][this.sockets[key].indexOf(socket)] = newSocket;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
destroy() {
|
|
59
|
+
for (const property in this.sockets) {
|
|
60
|
+
if (this.sockets.hasOwnProperty(property)) {
|
|
61
|
+
const sockets = this.sockets[property];
|
|
62
|
+
sockets.forEach((socket) => {
|
|
63
|
+
socket.destroy();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = class TunnelAgent extends events.EventEmitter {
|
|
71
|
+
request;
|
|
72
|
+
options;
|
|
73
|
+
defaultPort;
|
|
74
|
+
maxSockets;
|
|
75
|
+
requests;
|
|
76
|
+
sockets;
|
|
77
|
+
freeSockets;
|
|
78
|
+
keepAlive;
|
|
79
|
+
proxyOptions;
|
|
80
|
+
destroyPending = false;
|
|
81
|
+
agentId;
|
|
82
|
+
createSocket;
|
|
83
|
+
|
|
84
|
+
debugLog(message) {
|
|
85
|
+
debug('[' + this.agentId + ']: ' + message);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
constructor(options, proxyOverHttps = false, targetUsesHttps = false) {
|
|
89
|
+
super();
|
|
90
|
+
const self = this;
|
|
91
|
+
this.options = options;
|
|
92
|
+
this.proxyOptions = options.proxy || {};
|
|
93
|
+
this.maxSockets = options.maxSockets || 1;
|
|
94
|
+
this.keepAlive = options.keepAlive || false;
|
|
95
|
+
this.requests = [];
|
|
96
|
+
this.sockets = new SocketStore();
|
|
97
|
+
this.freeSockets = new SocketStore();
|
|
98
|
+
this.request = proxyOverHttps ? https.request : http.request;
|
|
99
|
+
this.createSocket = targetUsesHttps ? this.createSecureSocket : this.createTcpSocket;
|
|
100
|
+
this.defaultPort = targetUsesHttps ? 443 : 80;
|
|
101
|
+
this.agentId = agentCount++;
|
|
102
|
+
|
|
103
|
+
// attempt to negotiate http/1.1 for proxy servers that support http/2
|
|
104
|
+
if (this.proxyOptions.secureProxy && !('ALPNProtocols' in this.proxyOptions)) {
|
|
105
|
+
this.proxyOptions.ALPNProtocols = ['http 1.1'];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
self.on('free', function onFree(socket, request) {
|
|
109
|
+
for (let i = 0, len = self.requests.length; i < len; ++i) {
|
|
110
|
+
const pending = self.requests[i];
|
|
111
|
+
if (pending.socketKey === request.socketKey) {
|
|
112
|
+
self.debugLog('socket free, reusing for pending request');
|
|
113
|
+
// Detect the request to connect same origin server, reuse the connection.
|
|
114
|
+
self.requests.splice(i, 1);
|
|
115
|
+
pending.clientReq.reusedSocket = true;
|
|
116
|
+
pending.clientReq.onSocket(socket);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
self.sockets.remove(request.socketKey, socket);
|
|
122
|
+
if (!self.keepAlive) {
|
|
123
|
+
socket.destroy();
|
|
124
|
+
self.debugLog('socket free, non keep-alive => destroy socket');
|
|
125
|
+
} else {
|
|
126
|
+
// save the socket for reuse later
|
|
127
|
+
socket.removeAllListeners();
|
|
128
|
+
socket.unref();
|
|
129
|
+
self.freeSockets.insert(request.socketKey, socket);
|
|
130
|
+
socket.once('close', (_) => {
|
|
131
|
+
if (self.destroyPending) return;
|
|
132
|
+
self.debugLog('remove socket on socket close');
|
|
133
|
+
self.freeSockets.remove(request.socketKey, socket);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
self.processPending();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Counts all sockets active in requests and pending (keep-alive)
|
|
142
|
+
*
|
|
143
|
+
* @returns {number} The number of sockets, free and in use
|
|
144
|
+
*/
|
|
145
|
+
socketCount() {
|
|
146
|
+
return this.sockets.count() + this.freeSockets.count();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
addRequest(req, _opts) {
|
|
150
|
+
const self = this;
|
|
151
|
+
const request = {
|
|
152
|
+
clientReq: req,
|
|
153
|
+
socketKey: `${_opts.host}:${_opts.port}`,
|
|
154
|
+
options: { ..._opts, ...self.options },
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (self.sockets.length(request.socketKey) >= this.maxSockets) {
|
|
158
|
+
// We are over limit for the host so we'll add it to the queue.
|
|
159
|
+
self.requests.push(request);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (self.keepAlive) {
|
|
164
|
+
const socket = self.freeSockets.get(request.socketKey);
|
|
165
|
+
if (socket) {
|
|
166
|
+
this.debugLog('addRequest: reuse free socket for ' + request.socketKey);
|
|
167
|
+
socket.removeAllListeners();
|
|
168
|
+
socket.ref();
|
|
169
|
+
self.sockets.insert(request.socketKey, socket);
|
|
170
|
+
req.reusedSocket = true;
|
|
171
|
+
self.executeRequest(request, socket);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// If we are under maxSockets create a new one.
|
|
177
|
+
self.createSocket(request);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
executeRequest(request, socket) {
|
|
181
|
+
const self = this;
|
|
182
|
+
socket.on('free', onFree);
|
|
183
|
+
socket.on('close', onCloseOrRemove);
|
|
184
|
+
socket.on('agentRemove', onCloseOrRemove);
|
|
185
|
+
request.clientReq.onSocket(socket);
|
|
186
|
+
|
|
187
|
+
function onFree() {
|
|
188
|
+
self.debugLog('onFree');
|
|
189
|
+
self.emit('free', socket, request);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function onCloseOrRemove(hadError) {
|
|
193
|
+
self.debugLog('onClose');
|
|
194
|
+
if (self.destroyPending) return;
|
|
195
|
+
socket.removeListener('free', onFree);
|
|
196
|
+
socket.removeListener('close', onCloseOrRemove);
|
|
197
|
+
socket.removeListener('agentRemove', onCloseOrRemove);
|
|
198
|
+
if (self.keepAlive) {
|
|
199
|
+
socket.emit('close', hadError); // Let the freeSocket event handler remove the socket
|
|
200
|
+
}
|
|
201
|
+
self.processPending();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
escapeHost(hostname, port) {
|
|
206
|
+
if (hostname.indexOf(':') === -1) {
|
|
207
|
+
return `${hostname}:${port}`;
|
|
208
|
+
}
|
|
209
|
+
return `[${hostname}]:${port}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
createSocketInternal(request, cb) {
|
|
213
|
+
const self = this;
|
|
214
|
+
const host = this.escapeHost(request.options.host, request.options.port);
|
|
215
|
+
const connectOptions = {
|
|
216
|
+
...self.proxyOptions,
|
|
217
|
+
method: 'CONNECT',
|
|
218
|
+
path: host,
|
|
219
|
+
headers: {
|
|
220
|
+
host: host,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
if (request.options.localAddress) {
|
|
224
|
+
connectOptions.localAddress = request.options.localAddress;
|
|
225
|
+
}
|
|
226
|
+
if (self.proxyOptions.proxyAuth) {
|
|
227
|
+
connectOptions.headers = connectOptions.headers || {};
|
|
228
|
+
connectOptions.headers['Proxy-Authorization'] = 'Basic ' + Buffer.from(self.proxyOptions.proxyAuth).toString('base64');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const connectReq = self.request(connectOptions);
|
|
232
|
+
connectReq.once('connect', onConnect);
|
|
233
|
+
connectReq.once('error', onError);
|
|
234
|
+
connectReq.end();
|
|
235
|
+
|
|
236
|
+
function onConnect(res, socket, head) {
|
|
237
|
+
connectReq.removeAllListeners();
|
|
238
|
+
socket.removeAllListeners();
|
|
239
|
+
|
|
240
|
+
if (res.statusCode !== 200) {
|
|
241
|
+
self.debugLog('tunneling socket could not be established, statusCode=' + res.statusCode);
|
|
242
|
+
socket.destroy();
|
|
243
|
+
request.clientReq.destroy(new Error('tunneling socket could not be established, ' + 'statusCode=' + res.statusCode));
|
|
244
|
+
self.processPending();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (head.length > 0) {
|
|
248
|
+
self.debugLog('got illegal response body from proxy');
|
|
249
|
+
socket.destroy();
|
|
250
|
+
request.clientReq.destroy(new Error('got illegal response body from proxy'));
|
|
251
|
+
self.processPending();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
self.debugLog('tunneling connection established');
|
|
255
|
+
self.sockets.insert(request.socketKey, socket);
|
|
256
|
+
return cb(socket);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function onError(cause) {
|
|
260
|
+
connectReq.removeAllListeners();
|
|
261
|
+
self.debugLog('tunneling socket could not be established, cause=' + cause.message + '\n' + cause.stack);
|
|
262
|
+
request.clientReq.destroy(new Error('tunneling socket could not be established, ' + 'cause=' + cause.message));
|
|
263
|
+
self.processPending();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
processPending() {
|
|
268
|
+
const pending = this.requests.shift();
|
|
269
|
+
if (pending) {
|
|
270
|
+
// If we have pending requests and a socket gets closed a new one
|
|
271
|
+
// needs to be created to take over in the pool for the one that closed.
|
|
272
|
+
this.createSocket(pending);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
createTcpSocket(request) {
|
|
277
|
+
const self = this;
|
|
278
|
+
self.createSocketInternal(request, (socket) => self.executeRequest(request, socket));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
createSecureSocket(request) {
|
|
282
|
+
const self = this;
|
|
283
|
+
self.createSocketInternal(request, function (socket) {
|
|
284
|
+
const hostHeader = request.clientReq.getHeader('host');
|
|
285
|
+
const tlsOptions = {
|
|
286
|
+
...omit(self.options, 'host', 'path', 'port'),
|
|
287
|
+
socket: socket,
|
|
288
|
+
};
|
|
289
|
+
let servername = '';
|
|
290
|
+
if (hostHeader) {
|
|
291
|
+
servername = new URL('https://' + hostHeader).hostname;
|
|
292
|
+
} else if (request.options.host) {
|
|
293
|
+
servername = request.options.host;
|
|
294
|
+
}
|
|
295
|
+
if (servername) {
|
|
296
|
+
tlsOptions.servername = servername;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const secureSocket = tls.connect(0, tlsOptions);
|
|
300
|
+
self.sockets.replace(request.socketKey, socket, secureSocket);
|
|
301
|
+
self.executeRequest(request, secureSocket);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
destroy() {
|
|
306
|
+
this.debugLog('destroying agent');
|
|
307
|
+
this.destroyPending = true;
|
|
308
|
+
this.sockets.destroy();
|
|
309
|
+
this.freeSockets.destroy();
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
function omit(obj, ...keys) {
|
|
314
|
+
const ret = {};
|
|
315
|
+
for (var key in obj) {
|
|
316
|
+
if (!keys.includes(key)) {
|
|
317
|
+
ret[key] = obj[key];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return ret;
|
|
321
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
123456789012345
|
|
2
|
+
123456789012345
|
|
3
|
+
123456789012345
|
|
4
|
+
123456789012345
|
|
5
|
+
123456789012345
|
|
6
|
+
123456789012345
|
|
7
|
+
123456789012345
|
|
8
|
+
123456789012345
|
|
9
|
+
123456789012345
|
|
10
|
+
123456789012345
|
|
11
|
+
123456789012345
|
|
12
|
+
123456789012345
|
|
13
|
+
123456789012345
|
|
14
|
+
123456789012345
|
|
15
|
+
123456789012345
|
|
16
|
+
123456789012345
|
|
17
|
+
123456789012345
|
|
18
|
+
123456789012345
|
|
19
|
+
123456789012345
|
|
20
|
+
123456789012345
|
|
21
|
+
123456789012345
|
|
22
|
+
123456789012345
|
|
23
|
+
123456789012345
|
|
24
|
+
123456789012345
|
|
25
|
+
123456789012345
|
|
26
|
+
123456789012345
|
|
27
|
+
123456789012345
|
|
28
|
+
123456789012345
|
|
29
|
+
123456789012345
|
|
30
|
+
123456789012345
|
|
31
|
+
123456789012345
|
|
32
|
+
123456789012345
|
|
33
|
+
123456789012345
|
|
34
|
+
123456789012345
|
|
35
|
+
123456789012345
|
|
36
|
+
123456789012345
|
|
37
|
+
123456789012345
|
|
38
|
+
123456789012345
|
|
39
|
+
123456789012345
|
|
40
|
+
123456789012345
|
|
41
|
+
123456789012345
|
|
42
|
+
123456789012345
|
|
43
|
+
123456789012345
|
|
44
|
+
123456789012345
|
|
45
|
+
123456789012345
|
|
46
|
+
123456789012345
|
|
47
|
+
123456789012345
|
|
48
|
+
123456789012345
|
|
49
|
+
123456789012345
|
|
50
|
+
123456789012345
|
|
51
|
+
123456789012345
|
|
52
|
+
123456789012345
|
|
53
|
+
123456789012345
|
|
54
|
+
123456789012345
|
|
55
|
+
123456789012345
|
|
56
|
+
123456789012345
|
|
57
|
+
123456789012345
|
|
58
|
+
123456789012345
|
|
59
|
+
123456789012345
|
|
60
|
+
123456789012345
|
|
61
|
+
123456789012345
|
|
62
|
+
123456789012345
|
|
63
|
+
123456789012345
|
|
64
|
+
123456789012345
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
123456789012345
|
|
2
|
+
123456789012345
|
|
3
|
+
123456789012345
|
|
4
|
+
123456789012345
|
|
5
|
+
123456789012345
|
|
6
|
+
123456789012345
|
|
7
|
+
123456789012345
|
|
8
|
+
123456789012345
|
|
9
|
+
123456789012345
|
|
10
|
+
123456789012345
|
|
11
|
+
123456789012345
|
|
12
|
+
123456789012345
|
|
13
|
+
123456789012345
|
|
14
|
+
123456789012345
|
|
15
|
+
123456789012345
|
|
16
|
+
123456789012345
|
|
17
|
+
123456789012345
|
|
18
|
+
123456789012345
|
|
19
|
+
123456789012345
|
|
20
|
+
123456789012345
|
|
21
|
+
123456789012345
|
|
22
|
+
123456789012345
|
|
23
|
+
123456789012345
|
|
24
|
+
123456789012345
|
|
25
|
+
123456789012345
|
|
26
|
+
123456789012345
|
|
27
|
+
123456789012345
|
|
28
|
+
123456789012345
|
|
29
|
+
123456789012345
|
|
30
|
+
123456789012345
|
|
31
|
+
123456789012345
|
|
32
|
+
123456789012345
|
|
33
|
+
123456789012345
|
|
34
|
+
123456789012345
|
|
35
|
+
123456789012345
|
|
36
|
+
123456789012345
|
|
37
|
+
123456789012345
|
|
38
|
+
123456789012345
|
|
39
|
+
123456789012345
|
|
40
|
+
123456789012345
|
|
41
|
+
123456789012345
|
|
42
|
+
123456789012345
|
|
43
|
+
123456789012345
|
|
44
|
+
123456789012345
|
|
45
|
+
123456789012345
|
|
46
|
+
123456789012345
|
|
47
|
+
123456789012345
|
|
48
|
+
123456789012345
|
|
49
|
+
123456789012345
|
|
50
|
+
123456789012345
|
|
51
|
+
123456789012345
|
|
52
|
+
123456789012345
|
|
53
|
+
123456789012345
|
|
54
|
+
123456789012345
|
|
55
|
+
123456789012345
|
|
56
|
+
123456789012345
|
|
57
|
+
123456789012345
|
|
58
|
+
123456789012345
|
|
59
|
+
123456789012345
|
|
60
|
+
123456789012345
|
|
61
|
+
123456789012345
|
|
62
|
+
123456789012345
|
|
63
|
+
123456789012345
|
|
64
|
+
123456789012345
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
123456789012345
|
|
2
|
+
123456789012345
|
|
3
|
+
123456789012345
|
|
4
|
+
123456789012345
|
|
5
|
+
123456789012345
|
|
6
|
+
123456789012345
|
|
7
|
+
123456789012345
|
|
8
|
+
123456789012345
|
|
9
|
+
123456789012345
|
|
10
|
+
123456789012345
|
|
11
|
+
123456789012345
|
|
12
|
+
123456789012345
|
|
13
|
+
123456789012345
|
|
14
|
+
123456789012345
|
|
15
|
+
123456789012345
|
|
16
|
+
123456789012345
|
|
17
|
+
123456789012345
|
|
18
|
+
123456789012345
|
|
19
|
+
123456789012345
|
|
20
|
+
123456789012345
|
|
21
|
+
123456789012345
|
|
22
|
+
123456789012345
|
|
23
|
+
123456789012345
|
|
24
|
+
123456789012345
|
|
25
|
+
123456789012345
|
|
26
|
+
123456789012345
|
|
27
|
+
123456789012345
|
|
28
|
+
123456789012345
|
|
29
|
+
123456789012345
|
|
30
|
+
123456789012345
|
|
31
|
+
123456789012345
|
|
32
|
+
123456789012345
|
|
33
|
+
123456789012345
|
|
34
|
+
123456789012345
|
|
35
|
+
123456789012345
|
|
36
|
+
123456789012345
|
|
37
|
+
123456789012345
|
|
38
|
+
123456789012345
|
|
39
|
+
123456789012345
|
|
40
|
+
123456789012345
|
|
41
|
+
123456789012345
|
|
42
|
+
123456789012345
|
|
43
|
+
123456789012345
|
|
44
|
+
123456789012345
|
|
45
|
+
123456789012345
|
|
46
|
+
123456789012345
|
|
47
|
+
123456789012345
|
|
48
|
+
123456789012345
|
|
49
|
+
123456789012345
|
|
50
|
+
123456789012345
|
|
51
|
+
123456789012345
|
|
52
|
+
123456789012345
|
|
53
|
+
123456789012345
|
|
54
|
+
123456789012345
|
|
55
|
+
123456789012345
|
|
56
|
+
123456789012345
|
|
57
|
+
123456789012345
|
|
58
|
+
123456789012345
|
|
59
|
+
123456789012345
|
|
60
|
+
123456789012345
|
|
61
|
+
123456789012345
|
|
62
|
+
123456789012345
|
|
63
|
+
123456789012345
|
|
64
|
+
123456789012345
|
|
File without changes
|