Compare commits
1217 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fd38dbf12 | ||
|
|
6210288085 | ||
|
|
11742db503 | ||
|
|
b4195b15c6 | ||
|
|
519840761c | ||
|
|
4bd174b96f | ||
|
|
57fbb59889 | ||
|
|
64839bd61b | ||
|
|
e81d50162f | ||
|
|
2442571e2f | ||
|
|
da7aaf3ec4 | ||
|
|
59b8bcf817 | ||
|
|
4d6a4471b2 | ||
|
|
9cde5adfd4 | ||
|
|
7b03059819 | ||
|
|
8dea48ed5b | ||
|
|
397a5301fb | ||
|
|
17847ae415 | ||
|
|
ebb033ce1a | ||
|
|
5d7e0bc1e5 | ||
|
|
f80ff856ba | ||
|
|
0582e05edc | ||
|
|
54dbf115ba | ||
|
|
506ae57aa3 | ||
|
|
429d30a390 | ||
|
|
a3bb5d1363 | ||
|
|
b29de5c8fd | ||
|
|
cf7f4a6409 | ||
|
|
a5b33bbffc | ||
|
|
97f8c120aa | ||
|
|
ec0746fc8f | ||
|
|
7c88cb4392 | ||
|
|
c30555c699 | ||
|
|
ae413e13be | ||
|
|
dd623dc3a0 | ||
|
|
04710d8fc0 | ||
|
|
23bcf6fb86 | ||
|
|
2c3a8f9d42 | ||
|
|
e4f7f32f91 | ||
|
|
552527c085 | ||
|
|
5d73dcaf89 | ||
|
|
5e8b26893e | ||
|
|
93aae05e63 | ||
|
|
a91a0d3bef | ||
|
|
3f96ce3665 | ||
|
|
15bd1383be | ||
|
|
17e81b24b9 | ||
|
|
a9481cc941 | ||
|
|
5c51e485f6 | ||
|
|
b00be1baf5 | ||
|
|
8153e1b109 | ||
|
|
25f9a8dadb | ||
|
|
eb0c87d77a | ||
|
|
d0e0bc4ff3 | ||
|
|
0feccf9b4a | ||
|
|
836bb348fb | ||
|
|
b6ee1100ef | ||
|
|
fab8391daf | ||
|
|
368fe92a10 | ||
|
|
21df66d622 | ||
|
|
d7e6bae469 | ||
|
|
ba99fd5ba1 | ||
|
|
88a29f5928 | ||
|
|
32a1b6e6e2 | ||
|
|
8b93b579d3 | ||
|
|
b804e96f4a | ||
|
|
d53ce9750b | ||
|
|
644c0981db | ||
|
|
cda92ac5ab | ||
|
|
a038b282ec | ||
|
|
1e6c7dd6ea | ||
|
|
6f006fbf40 | ||
|
|
442305c62f | ||
|
|
00cf9c25bb | ||
|
|
5b52447408 | ||
|
|
4e2853bb79 | ||
|
|
cc32186328 | ||
|
|
bb080cae17 | ||
|
|
c4555cd64e | ||
|
|
14bbd93722 | ||
|
|
901a7bec62 | ||
|
|
fd0e9190f2 | ||
|
|
94fb6adf4f | ||
|
|
fdc9624d70 | ||
|
|
420a256832 | ||
|
|
5ba07d7420 | ||
|
|
124e89867f | ||
|
|
8847ed4e28 | ||
|
|
bd03fc34d2 | ||
|
|
8ae5fa0c0f | ||
|
|
c20bf97b5b | ||
|
|
f589b1be07 | ||
|
|
d725a81398 | ||
|
|
e7dcf3cc86 | ||
|
|
a4c829d836 | ||
|
|
ff96d2ec0c | ||
|
|
d0afe78e98 | ||
|
|
b1a396d9c5 | ||
|
|
b249e14a66 | ||
|
|
513f426623 | ||
|
|
7aa25ca0e3 | ||
|
|
e13f1f73fa | ||
|
|
95401ce6de | ||
|
|
f9ae0d0ffd | ||
|
|
fb7863439e | ||
|
|
1c9fbfce50 | ||
|
|
90854b2c51 | ||
|
|
672589b553 | ||
|
|
430425ce2f | ||
|
|
4a587825f3 | ||
|
|
2b418811ff | ||
|
|
aeb9580870 | ||
|
|
d1b30a58e2 | ||
|
|
4e435e1ac7 | ||
|
|
9fb0e14897 | ||
|
|
d8244e5f49 | ||
|
|
66f67c026d | ||
|
|
fc7d499bf7 | ||
|
|
acb6c8ca25 | ||
|
|
3a187945dc | ||
|
|
19490ffc51 | ||
|
|
f783ec3df3 | ||
|
|
de1c6b15fe | ||
|
|
7cbe4279dd | ||
|
|
a7926a72b5 | ||
|
|
e84ff197ec | ||
|
|
d72ee76d74 | ||
|
|
0f31b364e8 | ||
|
|
b0edff4963 | ||
|
|
b5fccd1ac9 | ||
|
|
acd4349b75 | ||
|
|
c9c22ed080 | ||
|
|
8ff89a7141 | ||
|
|
8ecf4ae0a9 | ||
|
|
3e78ad3471 | ||
|
|
ee8063401e | ||
|
|
51aa107cb8 | ||
|
|
855ace7f95 | ||
|
|
b64dbfe499 | ||
|
|
29294bdabb | ||
|
|
f9dea8a52c | ||
|
|
09656471de | ||
|
|
84388ff1da | ||
|
|
08c5358a8c | ||
|
|
24c1f92644 | ||
|
|
9c175a597b | ||
|
|
945f833ebd | ||
|
|
27470c0e8d | ||
|
|
3738221bf5 | ||
|
|
1465ce2b92 | ||
|
|
f8a7483b08 | ||
|
|
6be3c6841c | ||
|
|
ffdd682137 | ||
|
|
ceb44aa22b | ||
|
|
a6321a0961 | ||
|
|
2df4f7b628 | ||
|
|
2e5a1e7fd9 | ||
|
|
a6d6e58a80 | ||
|
|
68a559ebac | ||
|
|
beafd31cab | ||
|
|
fd2821fb92 | ||
|
|
1351f582b0 | ||
|
|
3a3f4ed960 | ||
|
|
2e3c8ed2cb | ||
|
|
b743ee7e3c | ||
|
|
5e086d1830 | ||
|
|
2e38c0ebae | ||
|
|
31be70a18c | ||
|
|
c96dd560cd | ||
|
|
5d924c0cf2 | ||
|
|
68fd3d15cc | ||
|
|
56d3610907 | ||
|
|
d072428249 | ||
|
|
cfffd04736 | ||
|
|
5b1ac962f2 | ||
|
|
53349fd096 | ||
|
|
5ee2faf125 | ||
|
|
5f345a90df | ||
|
|
6d12e40078 | ||
|
|
3ba8d0827b | ||
|
|
bffd8c3d7c | ||
|
|
d6e04be39f | ||
|
|
58b167268e | ||
|
|
7fef14c322 | ||
|
|
e3b1a33596 | ||
|
|
3639e32939 | ||
|
|
9707e0c336 | ||
|
|
e9f3d0d0c3 | ||
|
|
c9bf4ed20e | ||
|
|
999a65e412 | ||
|
|
cf57834019 | ||
|
|
16d7627ba6 | ||
|
|
3b4b8d94d5 | ||
|
|
18553ba130 | ||
|
|
e49846bb1b | ||
|
|
a6d3745cd4 | ||
|
|
b3a82bc294 | ||
|
|
4829e5286e | ||
|
|
7df6948fe6 | ||
|
|
44dfb877f5 | ||
|
|
5476cf6c21 | ||
|
|
789e7590bf | ||
|
|
4cc2e0b44b | ||
|
|
b55a0b6c31 | ||
|
|
e2180cf507 | ||
|
|
249fcff93a | ||
|
|
609a7e75ed | ||
|
|
3a518e3959 | ||
|
|
279e7d1212 | ||
|
|
a8df59bdf8 | ||
|
|
2a817ec25f | ||
|
|
2187a9e8b8 | ||
|
|
770ef82fe1 | ||
|
|
436fb47057 | ||
|
|
87bdc7013f | ||
|
|
7989000ef8 | ||
|
|
1867bd3184 | ||
|
|
cc96236dac | ||
|
|
807f170b72 | ||
|
|
109d80a39e | ||
|
|
4f59b0d2c6 | ||
|
|
1d07e65997 | ||
|
|
df87eb3224 | ||
|
|
7a5fe2afac | ||
|
|
7fd2357245 | ||
|
|
6cdf8f9007 | ||
|
|
e1d447c450 | ||
|
|
32709843fb | ||
|
|
02431bf71e | ||
|
|
ef347442be | ||
|
|
8e009d8fe8 | ||
|
|
432a6c4eaf | ||
|
|
da0618c442 | ||
|
|
4ea5ec7194 | ||
|
|
7f16f90186 | ||
|
|
71a22be8ca | ||
|
|
f504c6c181 | ||
|
|
c54f886525 | ||
|
|
d8177a7724 | ||
|
|
17bc128546 | ||
|
|
09b8c73e47 | ||
|
|
3618508640 | ||
|
|
2a25bce808 | ||
|
|
6e91aa88be | ||
|
|
1ae08b6b78 | ||
|
|
19f969d442 | ||
|
|
2f0609d6c7 | ||
|
|
aeed698ed2 | ||
|
|
6e1a04ecd8 | ||
|
|
559d4c0ece | ||
|
|
9640730d6c | ||
|
|
a702e69e16 | ||
|
|
492eeef721 | ||
|
|
b2008958f1 | ||
|
|
e41dad708e | ||
|
|
582285a37f | ||
|
|
1926522f99 | ||
|
|
96c9f459c1 | ||
|
|
15c6cb09fd | ||
|
|
0651c20990 | ||
|
|
eb1cb24723 | ||
|
|
912f4e4fc4 | ||
|
|
80aebfc26f | ||
|
|
f6ebdb9aec | ||
|
|
011a9c944b | ||
|
|
efb25cf611 | ||
|
|
a1c396ea27 | ||
|
|
8cfd437b7e | ||
|
|
9a4f81c2cb | ||
|
|
96c6604252 | ||
|
|
9036126359 | ||
|
|
73cc898f6b | ||
|
|
a5d0949bb4 | ||
|
|
c8b50ba2d3 | ||
|
|
226a0895a9 | ||
|
|
a77371e9b4 | ||
|
|
3ed7610e8d | ||
|
|
2351b733cd | ||
|
|
c76559a4d4 | ||
|
|
a5f44c464a | ||
|
|
51cb473141 | ||
|
|
99bc24c0ef | ||
|
|
5a7e08c4e5 | ||
|
|
9d89393695 | ||
|
|
b436e1dc88 | ||
|
|
172b271c6f | ||
|
|
8fbf77a76d | ||
|
|
9fdc251b83 | ||
|
|
2d4f1b2362 | ||
|
|
450c22359f | ||
|
|
ef360d6927 | ||
|
|
9db2372c05 | ||
|
|
2cceccad55 | ||
|
|
78b54060b5 | ||
|
|
da4c04a22e | ||
|
|
5ec951622d | ||
|
|
42fb6c3b14 | ||
|
|
d5a3d085aa | ||
|
|
fdd98ace07 | ||
|
|
733f13528d | ||
|
|
f21c3f0b26 | ||
|
|
6f67870f70 | ||
|
|
0e9d2454d8 | ||
|
|
b33f50a1ae | ||
|
|
f3b4fb68ef | ||
|
|
2ee4f38743 | ||
|
|
d2a4cf3264 | ||
|
|
1358d53dd9 | ||
|
|
ac456232bb | ||
|
|
181267f256 | ||
|
|
e799500893 | ||
|
|
75c3cbafd0 | ||
|
|
c7b44ddcaa | ||
|
|
fcae1fa91d | ||
|
|
09ca08baaf | ||
|
|
391e98de9f | ||
|
|
fa4952408a | ||
|
|
4cb0424d23 | ||
|
|
db3233075c | ||
|
|
1307181005 | ||
|
|
e9929c23e1 | ||
|
|
fe73eba54e | ||
|
|
f08e733cc4 | ||
|
|
a3ebb7942d | ||
|
|
c0bf077974 | ||
|
|
c1b2d09cd6 | ||
|
|
ce465f0b03 | ||
|
|
794689304f | ||
|
|
0cabd62dea | ||
|
|
7018fc99a2 | ||
|
|
c2741683b2 | ||
|
|
e58ef5ed9c | ||
|
|
fe2d954555 | ||
|
|
64c3ea445b | ||
|
|
163852615a | ||
|
|
717b8aa194 | ||
|
|
3e37e54158 | ||
|
|
16f8ae0f30 | ||
|
|
5336a0a611 | ||
|
|
f8dc1da537 | ||
|
|
8927d7d144 | ||
|
|
3b65f66584 | ||
|
|
6fe99a6f1b | ||
|
|
9626250140 | ||
|
|
a514e254a2 | ||
|
|
bf8c018fc5 | ||
|
|
1bdb651636 | ||
|
|
abc40b8e8e | ||
|
|
a1e6368c29 | ||
|
|
ba6543a322 | ||
|
|
2961d21124 | ||
|
|
45e4e66b4c | ||
|
|
b99d77ec29 | ||
|
|
addd3605f6 | ||
|
|
54729bffa2 | ||
|
|
01dc7dc1d8 | ||
|
|
ff05d779c4 | ||
|
|
93bea50176 | ||
|
|
7bf1a5a802 | ||
|
|
83979b3468 | ||
|
|
e04a6784dd | ||
|
|
dc8fe022bc | ||
|
|
502d20e2cf | ||
|
|
fd66425de6 | ||
|
|
19310b2078 | ||
|
|
aea51476da | ||
|
|
8e729c79d8 | ||
|
|
98e334b7fb | ||
|
|
3fd63e5348 | ||
|
|
e95064fef5 | ||
|
|
2215dc3397 | ||
|
|
4b9af22c95 | ||
|
|
54481095ce | ||
|
|
214b3dcde8 | ||
|
|
ecb9f52bf3 | ||
|
|
92df8bd178 | ||
|
|
445df5b30e | ||
|
|
57692cd8b1 | ||
|
|
f4058bb5c2 | ||
|
|
c80dd8958a | ||
|
|
24879f8198 | ||
|
|
24411f7a68 | ||
|
|
93d28f66b2 | ||
|
|
f4c584cfce | ||
|
|
a85978f2be | ||
|
|
90e1a66930 | ||
|
|
2a1f1a7dfa | ||
|
|
6fd47b333e | ||
|
|
5b8fe3aeb5 | ||
|
|
7c6c2d2932 | ||
|
|
babf679914 | ||
|
|
f8adc5e1a2 | ||
|
|
aaedc13d2c | ||
|
|
1e4ac1f842 | ||
|
|
4ee0ca29d4 | ||
|
|
b264e9f3df | ||
|
|
90328d5faf | ||
|
|
1d062075c4 | ||
|
|
acfd865bb0 | ||
|
|
ff7deb4712 | ||
|
|
09c60e68e0 | ||
|
|
318634f072 | ||
|
|
105a34f912 | ||
|
|
5a17a04090 | ||
|
|
7dde31546e | ||
|
|
472e8e94b0 | ||
|
|
3250bc5513 | ||
|
|
1092c89f86 | ||
|
|
977946a319 | ||
|
|
373c5cb348 | ||
|
|
fee0864ba1 | ||
|
|
6d8d05b0cc | ||
|
|
fcc40d65ae | ||
|
|
3457e0925d | ||
|
|
f282941075 | ||
|
|
e3e1485369 | ||
|
|
b05050da5b | ||
|
|
7970e52068 | ||
|
|
d79f12d45a | ||
|
|
e6561e10d4 | ||
|
|
b5acc054bf | ||
|
|
ce59c07b0b | ||
|
|
f6edac245d | ||
|
|
c21239a634 | ||
|
|
8372674d09 | ||
|
|
df697b9461 | ||
|
|
0ef1286c37 | ||
|
|
52cb33ffdd | ||
|
|
dd3a4b4821 | ||
|
|
896f083000 | ||
|
|
05b23fdadc | ||
|
|
fe6e503ec3 | ||
|
|
d824821b7a | ||
|
|
e5ad620a15 | ||
|
|
0a769008cf | ||
|
|
34bbde1a7c | ||
|
|
cb1a7af3c2 | ||
|
|
51b6c6006b | ||
|
|
580d3e77f7 | ||
|
|
7df6962f43 | ||
|
|
caabe0ebda | ||
|
|
cb37b38ee4 | ||
|
|
601caa8711 | ||
|
|
5150ecb973 | ||
|
|
1ce74482bc | ||
|
|
a18ac6bed6 | ||
|
|
13d653e4dd | ||
|
|
ea4f8474c1 | ||
|
|
62ef3d91b6 | ||
|
|
38518baf44 | ||
|
|
04e8638291 | ||
|
|
5eea1bac6c | ||
|
|
a57f99cbe0 | ||
|
|
338ea47b6c | ||
|
|
20f9436672 | ||
|
|
d11d9ea5a4 | ||
|
|
dc8abe99c3 | ||
|
|
ce65cfff23 | ||
|
|
adb40fc5f4 | ||
|
|
7d62824936 | ||
|
|
e267ef1f54 | ||
|
|
ad7cf21d0e | ||
|
|
d484bb4f2f | ||
|
|
d66aa7097a | ||
|
|
69d51e88d8 | ||
|
|
f51ea1f206 | ||
|
|
e50d143d96 | ||
|
|
92a9174603 | ||
|
|
4edbd47088 | ||
|
|
c166c20701 | ||
|
|
640240e5c2 | ||
|
|
2793914d80 | ||
|
|
90bb89660c | ||
|
|
e3af9f023c | ||
|
|
95c8a1fb82 | ||
|
|
32d7e8d891 | ||
|
|
ee99d1fb26 | ||
|
|
098675516b | ||
|
|
9350963f58 | ||
|
|
999abc5b73 | ||
|
|
c00456f6fe | ||
|
|
ea1ed4159e | ||
|
|
18d52a4103 | ||
|
|
4bb6a223be | ||
|
|
d4710164c7 | ||
|
|
ee2439d8da | ||
|
|
94f4cd2be1 | ||
|
|
2ed6d7f750 | ||
|
|
f1ff7d0eaf | ||
|
|
389b6d006c | ||
|
|
71d5e5f150 | ||
|
|
8c2329795d | ||
|
|
977e4adfc6 | ||
|
|
b22bb17d90 | ||
|
|
7031b9294f | ||
|
|
3bd1e82b54 | ||
|
|
af4df69a16 | ||
|
|
08864ce302 | ||
|
|
a00be0c20d | ||
|
|
4f22d6fec6 | ||
|
|
216d376598 | ||
|
|
773ac4922b | ||
|
|
3a131f380d | ||
|
|
9f06ea4b05 | ||
|
|
9e3ec11c70 | ||
|
|
f3bbc7957e | ||
|
|
8e8c8b67f5 | ||
|
|
dbb5fa2c9c | ||
|
|
b82861a5e8 | ||
|
|
6fa9ed7f07 | ||
|
|
fdd6a21102 | ||
|
|
8a0b20719e | ||
|
|
03c7142fa6 | ||
|
|
03340c1eae | ||
|
|
e588e281b6 | ||
|
|
cf8b906e54 | ||
|
|
59b8b6f037 | ||
|
|
0b7dc598b2 | ||
|
|
c609284d0f | ||
|
|
90c1ec9d36 | ||
|
|
b76546084c | ||
|
|
e984bd212b | ||
|
|
83de901c8a | ||
|
|
c9f7ba8004 | ||
|
|
01a6c22103 | ||
|
|
9f1b1619e8 | ||
|
|
ffb3c0e6d0 | ||
|
|
0470a02b2f | ||
|
|
6bbeff6d05 | ||
|
|
0bc3e6c8a0 | ||
|
|
53018704e0 | ||
|
|
e8c16cfb4b | ||
|
|
27ea2f91c0 | ||
|
|
6c6646873c | ||
|
|
223d6492d4 | ||
|
|
ccd3d26059 | ||
|
|
f63e690b15 | ||
|
|
674057b1af | ||
|
|
014a0b53ae | ||
|
|
32cc6ea617 | ||
|
|
d6dd63f012 | ||
|
|
0210752ea5 | ||
|
|
3c060766e7 | ||
|
|
7915bc3b59 | ||
|
|
69c5628184 | ||
|
|
03d66b0664 | ||
|
|
7c82d7e485 | ||
|
|
c927517e37 | ||
|
|
9fd42ffb34 | ||
|
|
a7c4285a68 | ||
|
|
eb6d30537b | ||
|
|
b9b61c7e97 | ||
|
|
eebc466e34 | ||
|
|
f84ca18798 | ||
|
|
831083a976 | ||
|
|
7530aee68a | ||
|
|
18cc0495ef | ||
|
|
047fbb22f8 | ||
|
|
4fa150e077 | ||
|
|
5e00e46a9b | ||
|
|
25e00a3172 | ||
|
|
3d9af3152e | ||
|
|
9f9c0fae71 | ||
|
|
3729c13a91 | ||
|
|
eec65929da | ||
|
|
6f7a28b359 | ||
|
|
a6fa0432ee | ||
|
|
6fb1dc2342 | ||
|
|
bb7138c3f6 | ||
|
|
c6923c9686 | ||
|
|
c20cf6d6e5 | ||
|
|
d42a5ad1c5 | ||
|
|
5e7faae788 | ||
|
|
e4e4a104fe | ||
|
|
2d5cbaf461 | ||
|
|
f89ca4423a | ||
|
|
548c7475d6 | ||
|
|
fea001efcf | ||
|
|
4050e063c0 | ||
|
|
564b91fba9 | ||
|
|
9fe9e38786 | ||
|
|
05fde696c7 | ||
|
|
b31198f685 | ||
|
|
1efd94dfe6 | ||
|
|
3b95710f27 | ||
|
|
8083df1361 | ||
|
|
6de559c020 | ||
|
|
7579bb21c0 | ||
|
|
5cdadbd7fa | ||
|
|
115a02b0eb | ||
|
|
7b4c105046 | ||
|
|
d8f2d95f92 | ||
|
|
dd406da3e2 | ||
|
|
cafbb5f76b | ||
|
|
cca5196752 | ||
|
|
86b9f512a5 | ||
|
|
06980e0cc1 | ||
|
|
e45fe466ee | ||
|
|
2c76a5f8ac | ||
|
|
3cbbb281a5 | ||
|
|
5fe64d41ec | ||
|
|
6301c3f169 | ||
|
|
1ccf645638 | ||
|
|
dfdcaa893e | ||
|
|
d260231737 | ||
|
|
ecded4a440 | ||
|
|
3f55fb4c0c | ||
|
|
b5d40b083a | ||
|
|
602f17c8b2 | ||
|
|
74e89e94e9 | ||
|
|
4be7a2028e | ||
|
|
4379821787 | ||
|
|
6b3617754c | ||
|
|
ee6cf0eb9f | ||
|
|
fde2321222 | ||
|
|
a9c749995a | ||
|
|
93245c0179 | ||
|
|
a1843ffd34 | ||
|
|
0d266be965 | ||
|
|
7c822d06f6 | ||
|
|
d370e5a6a9 | ||
|
|
3dd6442b94 | ||
|
|
cac6e89870 | ||
|
|
45edca2218 | ||
|
|
46d710e079 | ||
|
|
f1bb9f83e6 | ||
|
|
6b6247b009 | ||
|
|
8a68666096 | ||
|
|
6d01bffc79 | ||
|
|
c09d03b20d | ||
|
|
0da30ca089 | ||
|
|
b5c7895bb5 | ||
|
|
97386397c2 | ||
|
|
137fd5978f | ||
|
|
e8593c47cc | ||
|
|
74bb946a91 | ||
|
|
82756c3968 | ||
|
|
faaf1f537a | ||
|
|
5383b2fccb | ||
|
|
9294981d71 | ||
|
|
17bdfd904a | ||
|
|
6cb257e51f | ||
|
|
3c16e59a5c | ||
|
|
37f6591563 | ||
|
|
3e9932d866 | ||
|
|
6954898da0 | ||
|
|
a55dc65775 | ||
|
|
66324d718a | ||
|
|
553e463b7e | ||
|
|
d7c95d6cd6 | ||
|
|
bf40ceb064 | ||
|
|
554d31d119 | ||
|
|
8a3a03805e | ||
|
|
9047e28074 | ||
|
|
2ee12c7293 | ||
|
|
dc6b2f184a | ||
|
|
1a9596737e | ||
|
|
89353f9312 | ||
|
|
6d58d5920e | ||
|
|
dfef2b862f | ||
|
|
61fff273fc | ||
|
|
97c7c6814f | ||
|
|
025e23627f | ||
|
|
0a38a704fe | ||
|
|
798ba0d262 | ||
|
|
24e3b5efba | ||
|
|
c00cde0aa3 | ||
|
|
0af72f0c68 | ||
|
|
82539add26 | ||
|
|
76a1e8df80 | ||
|
|
3fe188df21 | ||
|
|
4ed27c6eda | ||
|
|
6ec76aa61f | ||
|
|
4a46ffe3a0 | ||
|
|
e47862be71 | ||
|
|
04a9f14b3c | ||
|
|
0aed9ee9f4 | ||
|
|
79e2ff2ee8 | ||
|
|
e5880841f3 | ||
|
|
c49b0699c2 | ||
|
|
bc91a05dd2 | ||
|
|
5b5f56f51c | ||
|
|
f558c78d32 | ||
|
|
ed3eb37c4f | ||
|
|
09318d1943 | ||
|
|
fad4f457ae | ||
|
|
fc3668885d | ||
|
|
c385404e23 | ||
|
|
a0ff6dbc9e | ||
|
|
484ba9dc25 | ||
|
|
848d591f0a | ||
|
|
c07d819ce9 | ||
|
|
ea50f3d3ed | ||
|
|
76bdc74670 | ||
|
|
4a4502967e | ||
|
|
5358d2b81b | ||
|
|
0f6404f4ea | ||
|
|
2b9bfcc0d9 | ||
|
|
950cfb2945 | ||
|
|
06bb301bab | ||
|
|
bad7aee4ec | ||
|
|
400288bb69 | ||
|
|
c549466737 | ||
|
|
841f77d36a | ||
|
|
e281b4e389 | ||
|
|
64adb19b85 | ||
|
|
b84bb568cd | ||
|
|
ecc5e9bfaf | ||
|
|
133bceb43f | ||
|
|
b338aefe45 | ||
|
|
feae33499e | ||
|
|
40baedaa78 | ||
|
|
2b99b23eec | ||
|
|
4592ec3657 | ||
|
|
cf71b9dc9e | ||
|
|
691bdb3a92 | ||
|
|
ae1f67d31f | ||
|
|
a425c5e2ff | ||
|
|
972551e838 | ||
|
|
637d51cba0 | ||
|
|
c71e8dc829 | ||
|
|
2f541130ab | ||
|
|
322f64d517 | ||
|
|
ca3cb6487d | ||
|
|
76ebb24b77 | ||
|
|
4c11a42d21 | ||
|
|
8259455ac5 | ||
|
|
1fe0cedbd0 | ||
|
|
09485451f7 | ||
|
|
c78390752a | ||
|
|
7687965219 | ||
|
|
100430227c | ||
|
|
148795918f | ||
|
|
31bd3c9db7 | ||
|
|
037b3aea6a | ||
|
|
6a778e8c17 | ||
|
|
bac62f6969 | ||
|
|
6a14a277b2 | ||
|
|
67b4528643 | ||
|
|
7b34bb63c9 | ||
|
|
a854d07a66 | ||
|
|
7a7d794207 | ||
|
|
4fb5acad48 | ||
|
|
2e396e4d65 | ||
|
|
55ddeb7aaa | ||
|
|
907e1a8c43 | ||
|
|
8628c34b25 | ||
|
|
9938a6bf4b | ||
|
|
f308bfb4bf | ||
|
|
c0793c575d | ||
|
|
3c00c96b6a | ||
|
|
a1010be87a | ||
|
|
4b20afbd15 | ||
|
|
dc9859cd83 | ||
|
|
08f218724b | ||
|
|
daeb1eb2a3 | ||
|
|
326c7355ed | ||
|
|
d38606f409 | ||
|
|
29c45e3ebc | ||
|
|
20f748be12 | ||
|
|
1cb1e91ca1 | ||
|
|
7fb119c9f3 | ||
|
|
eda8dfe9ca | ||
|
|
4b3939ef4e | ||
|
|
cb0c79e79f | ||
|
|
e36638973f | ||
|
|
aef982d6e6 | ||
|
|
4a588f6512 | ||
|
|
b34cfb1fe0 | ||
|
|
4f028a107b | ||
|
|
91ee928f06 | ||
|
|
862b1eeed4 | ||
|
|
0786ec6108 | ||
|
|
2dca3114e9 | ||
|
|
3bf26bd89e | ||
|
|
ef074f8d6c | ||
|
|
4b46bff72f | ||
|
|
765d3b47a7 | ||
|
|
20c97527fb | ||
|
|
35b3efb058 | ||
|
|
8d1781489b | ||
|
|
92a33acf45 | ||
|
|
48a35b6a01 | ||
|
|
8db7c4355e | ||
|
|
d0a1013131 | ||
|
|
936c0b607b | ||
|
|
1123a440bc | ||
|
|
8f0039de92 | ||
|
|
f8b0ae6c6f | ||
|
|
188ab10871 | ||
|
|
a016077bf3 | ||
|
|
834c155da4 | ||
|
|
19b1d76e98 | ||
|
|
c43846f14e | ||
|
|
fd8d481be4 | ||
|
|
8576201cc1 | ||
|
|
0837025a74 | ||
|
|
5468c4e767 | ||
|
|
34c12de81c | ||
|
|
a5cd498f92 | ||
|
|
dbb588f031 | ||
|
|
720b98b715 | ||
|
|
efd921d876 | ||
|
|
1e28c2d949 | ||
|
|
a0c861ec5a | ||
|
|
8d841ea8e3 | ||
|
|
504430dcdd | ||
|
|
435ff1beeb | ||
|
|
6e247829a3 | ||
|
|
99795b747c | ||
|
|
3a325a1e04 | ||
|
|
734565dc05 | ||
|
|
697d5f0892 | ||
|
|
409d25739e | ||
|
|
65a792ba53 | ||
|
|
4239107ed2 | ||
|
|
f512925fd4 | ||
|
|
b740dd11cf | ||
|
|
430a479113 | ||
|
|
86394b25ee | ||
|
|
8086c6f0bf | ||
|
|
7742e11cde | ||
|
|
2597fcae80 | ||
|
|
5dd720cc21 | ||
|
|
fdbde9eb87 | ||
|
|
7e5227e4a1 | ||
|
|
60ae815c09 | ||
|
|
adecd99866 | ||
|
|
932fa34edc | ||
|
|
75fc0ae472 | ||
|
|
10433438d0 | ||
|
|
c84bc05d10 | ||
|
|
33412c4613 | ||
|
|
4bc965b45b | ||
|
|
6704abf988 | ||
|
|
d9f84677f8 | ||
|
|
e1880ce19e | ||
|
|
478d75fec4 | ||
|
|
77700e72b1 | ||
|
|
5d4068d539 | ||
|
|
57d6fe7150 | ||
|
|
a8dc1b2f7b | ||
|
|
060dcb49b4 | ||
|
|
7981d55860 | ||
|
|
8aa6754875 | ||
|
|
4660dd3b7f | ||
|
|
58046a2ec7 | ||
|
|
008423d49b | ||
|
|
3eb09b8b07 | ||
|
|
888cfe3f1e | ||
|
|
4545076b0f | ||
|
|
854fd796ae | ||
|
|
77af6d4c17 | ||
|
|
21704c9f84 | ||
|
|
4f913f2468 | ||
|
|
859f1bb959 | ||
|
|
8d8963d37a | ||
|
|
c2d3fe9929 | ||
|
|
3809d6eb32 | ||
|
|
490340fbb0 | ||
|
|
a528811e35 | ||
|
|
9275b2cc85 | ||
|
|
48b7031074 | ||
|
|
41d4997ea7 | ||
|
|
ff8a5e99ec | ||
|
|
9a14a6e022 | ||
|
|
404acfae49 | ||
|
|
1e438d7dae | ||
|
|
e929e089d0 | ||
|
|
81486f412f | ||
|
|
5cc0187b67 | ||
|
|
d9accbb6a7 | ||
|
|
17d8ab7dcd | ||
|
|
3f746eb7c8 | ||
|
|
9816059485 | ||
|
|
fe058b716b | ||
|
|
dc7d7dfd53 | ||
|
|
3e514c2c3b | ||
|
|
703adeaffc | ||
|
|
c4e5cb71b4 | ||
|
|
73572b0839 | ||
|
|
88f3944f5a | ||
|
|
20524118ef | ||
|
|
68bf469ad4 | ||
|
|
21f7f03cf7 | ||
|
|
96126e5a2f | ||
|
|
490b04f298 | ||
|
|
abacb64479 | ||
|
|
2a299c9201 | ||
|
|
39083b819f | ||
|
|
228f1512d7 | ||
|
|
f5b9e0e0d5 | ||
|
|
daaee2b971 | ||
|
|
a38cf312b2 | ||
|
|
ab44907dac | ||
|
|
1246f12f5a | ||
|
|
99b8c31d3a | ||
|
|
dbf3301ff9 | ||
|
|
a6fe4a1516 | ||
|
|
277f1023c9 | ||
|
|
820ae3f27c | ||
|
|
d52cfe8bac | ||
|
|
f74d7cbd3e | ||
|
|
7e9b5944fd | ||
|
|
6fb0be9cc0 | ||
|
|
1ddc1c68dc | ||
|
|
8b3dc9072f | ||
|
|
112e551240 | ||
|
|
7b4bac1874 | ||
|
|
baa7f78e83 | ||
|
|
45e4208218 | ||
|
|
1e48eacbf8 | ||
|
|
9e062994fe | ||
|
|
4f155f5004 | ||
|
|
c0ee2db8a4 | ||
|
|
5464b449ce | ||
|
|
f2e433d52b | ||
|
|
11fdc9340e | ||
|
|
1e71e2faa0 | ||
|
|
2806de2a5d | ||
|
|
27c032d53f | ||
|
|
c47f73f799 | ||
|
|
2901e619cb | ||
|
|
5a98db9589 | ||
|
|
34b631fa12 | ||
|
|
bb129c1151 | ||
|
|
37afd429ec | ||
|
|
edee08a778 | ||
|
|
0395d958cf | ||
|
|
9e51798295 | ||
|
|
cb6e2cd7f6 | ||
|
|
a79fa68deb | ||
|
|
fabf3eff4c | ||
|
|
6a60650073 | ||
|
|
6eada0f22e | ||
|
|
5241ddfe84 | ||
|
|
bea03619d7 | ||
|
|
8f03805697 | ||
|
|
d0c2ac0f9d | ||
|
|
d820a030a1 | ||
|
|
b4c0b89365 | ||
|
|
752d0f62f0 | ||
|
|
9404e811a5 | ||
|
|
2102e71c10 | ||
|
|
f023826a59 | ||
|
|
f03512d32b | ||
|
|
7f56593ee3 | ||
|
|
2995858bb0 | ||
|
|
a8cdfcd7cd | ||
|
|
4470cb7d55 | ||
|
|
4b06026d2e | ||
|
|
7967d5857d | ||
|
|
4ea84e8b74 | ||
|
|
bf63b9f738 | ||
|
|
8af77df0e0 | ||
|
|
df49fb84c1 | ||
|
|
d657e180d5 | ||
|
|
0afd9d1bb3 | ||
|
|
f839837f34 | ||
|
|
a030e1110d | ||
|
|
dabf5e1c9a | ||
|
|
bb9612441c | ||
|
|
48825a2e46 | ||
|
|
6a2a463b76 | ||
|
|
f1ec18dc4b | ||
|
|
6b9001ef6c | ||
|
|
9ff1203688 | ||
|
|
a891354a32 | ||
|
|
9c4c93695b | ||
|
|
3b510437a8 | ||
|
|
ed1ec0c27a | ||
|
|
526f61b136 | ||
|
|
568058d7d9 | ||
|
|
b77da91094 | ||
|
|
e2457e9e5c | ||
|
|
c6714cd591 | ||
|
|
58e98c7263 | ||
|
|
234deb4e7e | ||
|
|
4466cf6be1 | ||
|
|
cf5dddb174 | ||
|
|
70506d54ee | ||
|
|
09781df60a | ||
|
|
c730670711 | ||
|
|
691d62f403 | ||
|
|
a5d0094669 | ||
|
|
b65cf7afe3 | ||
|
|
e2e2c39148 | ||
|
|
de4dbd234a | ||
|
|
440f42415c | ||
|
|
8840ac4c1f | ||
|
|
9f662a69e1 | ||
|
|
c098eb4661 | ||
|
|
2c28f3549e | ||
|
|
616ec7626a | ||
|
|
9a915511b3 | ||
|
|
0d37933259 | ||
|
|
2add883370 | ||
|
|
e522347667 | ||
|
|
8987ee0b2a | ||
|
|
ceb9b5be1d | ||
|
|
6c258758c8 | ||
|
|
dadbf3a18b | ||
|
|
c18ac8015a | ||
|
|
68d3cc7295 | ||
|
|
8ebe73d1f1 | ||
|
|
75ea41a55b | ||
|
|
d519e8e763 | ||
|
|
bb6b31e7c0 | ||
|
|
5d19b66290 | ||
|
|
5422de642e | ||
|
|
02f8143097 | ||
|
|
3c7ae2f37d | ||
|
|
6075034521 | ||
|
|
d1a1a8f4cd | ||
|
|
54535893ab | ||
|
|
8c05cfe6a2 | ||
|
|
6c838c7947 | ||
|
|
f3f4fdc4ac | ||
|
|
985d705ad2 | ||
|
|
6234f8cba9 | ||
|
|
c0e34890e4 | ||
|
|
b42687d230 | ||
|
|
4ce0142651 | ||
|
|
b44255de3c | ||
|
|
b0fa7ee2d1 | ||
|
|
1c17291654 | ||
|
|
3b2bd161b7 | ||
|
|
d29e22ed4b | ||
|
|
1a0f577689 | ||
|
|
a3a59aa8f3 | ||
|
|
3a31064ded | ||
|
|
151136381e | ||
|
|
503b60a45b | ||
|
|
225c2bb65f | ||
|
|
8ea985ff6b | ||
|
|
0cec69ee1d | ||
|
|
d5d2a8bdcc | ||
|
|
693aa576b4 | ||
|
|
3abce5e2b6 | ||
|
|
5c6d6301ef | ||
|
|
f43ec71ccb | ||
|
|
ef61515f28 | ||
|
|
9094d0742f | ||
|
|
ede09b5c77 | ||
|
|
19e4d78197 | ||
|
|
380ec1e917 | ||
|
|
ee16b06b3a | ||
|
|
8149f883b0 | ||
|
|
c75249b5b0 | ||
|
|
942a7747ac | ||
|
|
96b336acc3 | ||
|
|
520a3b0220 | ||
|
|
230750ff88 | ||
|
|
84a6e36d84 | ||
|
|
203f0300e3 | ||
|
|
1cb12fec35 | ||
|
|
e5195ecfb5 | ||
|
|
7ee2361700 | ||
|
|
47a92ff273 | ||
|
|
fc99692219 | ||
|
|
bd7dde229a | ||
|
|
21a5c788e1 | ||
|
|
0b286d1fe3 | ||
|
|
987f540cde | ||
|
|
a11da1545a | ||
|
|
d149252a3b | ||
|
|
ed3386f044 | ||
|
|
36fa1ba655 | ||
|
|
77182a755d | ||
|
|
f05a4784fd | ||
|
|
7f7c056ecd | ||
|
|
bbb402f762 | ||
|
|
4d70d8065e | ||
|
|
3617a91f87 | ||
|
|
03445c4c49 | ||
|
|
2f45649a2c | ||
|
|
08d1011433 | ||
|
|
6e71913c46 | ||
|
|
cc7165dd3c | ||
|
|
a257fc3962 | ||
|
|
6a295ac196 | ||
|
|
ff52b00a7c | ||
|
|
9ee8d484db | ||
|
|
7b0ab14d4c | ||
|
|
1d109f8071 | ||
|
|
75e78b4137 | ||
|
|
2e4a4b1dc1 | ||
|
|
98d2c3ae18 | ||
|
|
8f05e141c3 | ||
|
|
1df6d53612 | ||
|
|
13242c8237 | ||
|
|
fecffa2a39 | ||
|
|
6a7226bb83 | ||
|
|
fbb98b1101 | ||
|
|
9f2871f381 | ||
|
|
558598c6be | ||
|
|
e6ce764750 | ||
|
|
aca1dc57d7 | ||
|
|
b9d5066f82 | ||
|
|
15d46e3280 | ||
|
|
b2f6128e9c | ||
|
|
aba48ee6dd | ||
|
|
5da3c8ba58 | ||
|
|
3f8166fa19 | ||
|
|
dafcf8647c | ||
|
|
6ae26226eb | ||
|
|
3a407396b8 | ||
|
|
782abbb86d | ||
|
|
ec26083e81 | ||
|
|
fea98dfa59 | ||
|
|
78f856cf81 | ||
|
|
61658cfbdc | ||
|
|
4c2c4b87c2 | ||
|
|
5622b2283d | ||
|
|
8b314040d9 | ||
|
|
e35c02f8f1 | ||
|
|
f1d26652f3 | ||
|
|
acc2e571ff | ||
|
|
fa51797602 | ||
|
|
3ca3ac2dae | ||
|
|
012892600b | ||
|
|
674f944942 | ||
|
|
8a2a91c1a7 | ||
|
|
473d3d5c2f | ||
|
|
5b6f22c0b9 | ||
|
|
1a58a0f8d8 | ||
|
|
fcebeea3ee | ||
|
|
015623f9ae | ||
|
|
957f915d42 | ||
|
|
70f717a295 | ||
|
|
3596a12eb7 | ||
|
|
9ee525b317 | ||
|
|
b6082f1216 | ||
|
|
0aa687624d | ||
|
|
a76a443994 | ||
|
|
94d37441ae | ||
|
|
7a3255d1ab | ||
|
|
b9cb60da64 | ||
|
|
566f2c4896 | ||
|
|
459002128c | ||
|
|
bffc6db38f | ||
|
|
57dc1c64d9 | ||
|
|
8dcb735626 | ||
|
|
d59bdb0c37 | ||
|
|
2f15d1c0fa | ||
|
|
16730bb7b4 | ||
|
|
f38a897fa2 | ||
|
|
1556ce195a | ||
|
|
2675196672 | ||
|
|
77fccaa1e5 | ||
|
|
e95d22abf6 | ||
|
|
cd9f0d7044 | ||
|
|
38340fe194 | ||
|
|
8aa60a93c5 | ||
|
|
f5b542724b | ||
|
|
a503bce863 | ||
|
|
21ad8945d9 | ||
|
|
908f212b6f | ||
|
|
5efaa637e5 | ||
|
|
835841707a | ||
|
|
0b29b3d140 | ||
|
|
69eae02eae | ||
|
|
8cf27005c5 | ||
|
|
c3de74ebfd | ||
|
|
8c5628bfae | ||
|
|
f92a839a7f | ||
|
|
c8bb68a4aa | ||
|
|
0173c55ead | ||
|
|
57b34a7637 | ||
|
|
5dbc825892 | ||
|
|
1570cc348c | ||
|
|
ac2ea00b2b | ||
|
|
361d653613 | ||
|
|
8fe23d3393 | ||
|
|
9f80a0046e | ||
|
|
64a0ab8be7 | ||
|
|
b7e4bfdd14 | ||
|
|
35235db7a0 | ||
|
|
89f232b5ec | ||
|
|
102b125f14 | ||
|
|
683f4657e4 | ||
|
|
6dc6f31579 | ||
|
|
ff4778e3c6 | ||
|
|
7dbe61b8d5 | ||
|
|
9b57669bbe | ||
|
|
709ffe8e39 | ||
|
|
11bf03f130 | ||
|
|
e01d30adb9 | ||
|
|
4791d0e707 | ||
|
|
34a52f09a2 | ||
|
|
5f9e3aebc5 | ||
|
|
fa1c4f30ee | ||
|
|
9374872a64 | ||
|
|
6c78876dcd | ||
|
|
bdd748c6e2 | ||
|
|
2ee792633c | ||
|
|
e351559dd2 | ||
|
|
764dde38f1 | ||
|
|
d91bef5ca0 | ||
|
|
c84413517f | ||
|
|
d4ddf4a3e3 | ||
|
|
f19f523e1a | ||
|
|
593e6195c7 | ||
|
|
86f02bda7d | ||
|
|
27844cd358 | ||
|
|
61727a771e | ||
|
|
693f222dc2 | ||
|
|
5eee99622f | ||
|
|
1eda590bfc | ||
|
|
b4688d140f | ||
|
|
dfd0459c03 | ||
|
|
d7eb7c9ef8 | ||
|
|
ad29d7fac7 | ||
|
|
c425cdab27 | ||
|
|
5aa335cd91 | ||
|
|
26e3e21121 | ||
|
|
4070eb7eb4 |
27
.babelrc
Normal file
27
.babelrc
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"presets": [
|
||||
"react",
|
||||
["env",{
|
||||
"targets": {
|
||||
"browsers": ["last 5 versions", "safari >= 11"],
|
||||
"loose": true
|
||||
}
|
||||
}],
|
||||
"stage-2",
|
||||
"stage-3"
|
||||
],
|
||||
"plugins": [
|
||||
"react-hot-loader/babel",
|
||||
"transform-runtime",
|
||||
"transform-object-rest-spread",
|
||||
"transform-decorators-legacy"
|
||||
],
|
||||
"env": {
|
||||
"development": {
|
||||
"presets": ["flow"],
|
||||
"plugins": [
|
||||
"flow-runtime"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
32
.dockerignore
Normal file
32
.dockerignore
Normal file
@@ -0,0 +1,32 @@
|
||||
# we try to aoid adding files to the docker images that change often
|
||||
# or that are not needed for running the docker image
|
||||
# tis greatly reduces the amount of times we need to rerun `npm install` when building image locally
|
||||
# https://codefresh.io/blog/not-ignore-dockerignore/
|
||||
# https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
|
||||
# consider them hidden
|
||||
.*
|
||||
# you can add exceptions like in .gitignore to maintain a whitelist:
|
||||
# e.g.
|
||||
!.babelrc
|
||||
!.eslintrc
|
||||
!.eslintignore
|
||||
!.stylelintrc
|
||||
|
||||
# not going to run tests inside the docker container
|
||||
test/
|
||||
|
||||
# do not copy over node_modules we will run `npm install` anyway
|
||||
node_modules
|
||||
|
||||
# output from test runs and similar things
|
||||
*.log
|
||||
coverage/
|
||||
|
||||
# IDE config files
|
||||
jsconfig.json
|
||||
*.iml
|
||||
|
||||
# let's not get to recursive ;)
|
||||
Dockerfile*
|
||||
docker-compose*.yaml
|
||||
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# 2 space indentation
|
||||
[{.,}*.{js,yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
coverage/
|
||||
wiki/
|
||||
static/
|
||||
80
.eslintrc
Normal file
80
.eslintrc
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"plugins": [
|
||||
"react",
|
||||
"flowtype"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"google",
|
||||
"plugin:react/recommended",
|
||||
"plugin:flowtype/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 7,
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"rules": {
|
||||
"no-tabs": 0,
|
||||
"keyword-spacing": 0,
|
||||
"padded-blocks": 0,
|
||||
"no-useless-escape": 0,
|
||||
"handle-callback-err": 2,
|
||||
"no-debugger": 2,
|
||||
"no-fallthrough": 2,
|
||||
"curly": 2,
|
||||
"eol-last": 1,
|
||||
"no-irregular-whitespace": 1,
|
||||
"no-mixed-spaces-and-tabs": [
|
||||
1,
|
||||
"smart-tabs"
|
||||
],
|
||||
"no-trailing-spaces": 1,
|
||||
"no-new-require": 2,
|
||||
"no-undef": 2,
|
||||
"no-unreachable": 2,
|
||||
"no-unused-vars": [
|
||||
2,
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "none"
|
||||
}
|
||||
],
|
||||
"max-len": [
|
||||
1,
|
||||
160
|
||||
],
|
||||
"semi": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"camelcase": 0,
|
||||
"require-jsdoc": 2,
|
||||
"valid-jsdoc": 2,
|
||||
"prefer-spread": 1,
|
||||
"prefer-rest-params": 1,
|
||||
"no-var": 2,
|
||||
"no-constant-condition": 2,
|
||||
"no-empty": 2,
|
||||
"guard-for-in": 2,
|
||||
"no-invalid-this": 2,
|
||||
"new-cap": 2,
|
||||
"one-var": 2,
|
||||
"no-console": [
|
||||
2,
|
||||
{
|
||||
"allow": [
|
||||
"warn"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
41
.github/issue_template.md
vendored
Normal file
41
.github/issue_template.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
#### My reason:
|
||||
|
||||
<!--
|
||||
a brief explanation of the issue, suggestion, feature
|
||||
-->
|
||||
|
||||
#### Steps to reproduce:
|
||||
|
||||
<!--
|
||||
(if it applies)
|
||||
how can I do in order to reproduce it? environment?
|
||||
-->
|
||||
#### App Version:
|
||||
<!--
|
||||
Define which version the issue happens and whether previous version the behaviour is correct
|
||||
-->
|
||||
|
||||
#### Config file:
|
||||
<!--
|
||||
Provide your config file might be really helpful. Please be aware to hide sensisive data before post.
|
||||
-->
|
||||
|
||||
#### Additional information:
|
||||
|
||||
<!--
|
||||
provide the following information would be helpful
|
||||
-->
|
||||
|
||||
- `$ set DEBUG=express:* verdaccio` enable extreme verdaccio debug mode
|
||||
- `$ npm --verbose` prints:
|
||||
- `$ npm config get registry` prints:
|
||||
- Verdaccio terminal output
|
||||
- Windows, OS X/macOS, or Linux?:
|
||||
- Verdaccio configuration file, eg: `cat ~/.config/verdaccio/config.yaml`
|
||||
<!-- Remove those does not apply for you -->
|
||||
- Container:
|
||||
- I use local environment
|
||||
- I develop / deploy using Docker.
|
||||
- I deploy to a PaaS.
|
||||
|
||||
#### Additional verbose log:
|
||||
16
.github/pull_request_template.md
vendored
Normal file
16
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
**Type:** bug / feature
|
||||
|
||||
The following has been addressed in the PR:
|
||||
|
||||
<!-- Remove the sections that your PR does not apply -->
|
||||
* There is a related issue
|
||||
* Unit or Functional tests are included in the PR
|
||||
|
||||
<!--
|
||||
Our bots should ensure:
|
||||
* The PR passes CI testing
|
||||
-->
|
||||
|
||||
**Description:**
|
||||
|
||||
Resolves #???
|
||||
33
.gitignore
vendored
33
.gitignore
vendored
@@ -1,5 +1,30 @@
|
||||
node_modules
|
||||
package.json
|
||||
npm-debug.log
|
||||
bin/storage
|
||||
bin/config.yaml
|
||||
verdaccio-*.tgz
|
||||
.DS_Store
|
||||
|
||||
###
|
||||
!bin/verdaccio
|
||||
test-storage*
|
||||
.verdaccio_test_env
|
||||
node_modules
|
||||
package-lock.json
|
||||
|
||||
|
||||
# Istanbul
|
||||
coverage/
|
||||
.nyc*
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
|
||||
.idea/
|
||||
|
||||
|
||||
# React
|
||||
bundle.js
|
||||
bundle.js.map
|
||||
__tests__
|
||||
__snapshots__
|
||||
|
||||
# Compiled script
|
||||
static/
|
||||
|
||||
10
.npmignore
Normal file
10
.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
coverage/
|
||||
verdaccio-*.tgz
|
||||
test-storage*
|
||||
scripts/
|
||||
wiki/
|
||||
src/webui
|
||||
tools/
|
||||
/.*
|
||||
8
.stylelintrc
Normal file
8
.stylelintrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"selector-pseudo-class-no-unknown": [true, {
|
||||
"ignorePseudoClasses": ["/global/"]
|
||||
}]
|
||||
}
|
||||
}
|
||||
11
.travis.yml
Normal file
11
.travis.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '4'
|
||||
- '6'
|
||||
- '7'
|
||||
- '8'
|
||||
sudo: false
|
||||
before_script:
|
||||
- npm install -g npm
|
||||
script: npm install && npm run build:webui && npm run test-travis
|
||||
after_success: npm run coverage:codecov
|
||||
74
AUTHORS
Normal file
74
AUTHORS
Normal file
@@ -0,0 +1,74 @@
|
||||
030 <chocolatey030@gmail.com>
|
||||
Alex Kocharin <alex@kocharin.ru>
|
||||
Alex Kocharin <rlidwka@kocharin.ru>
|
||||
Alex Vernacchia <avernacchia@exacttarget.com>
|
||||
Alexander Makarenko <estliberitas@gmail.com>
|
||||
Alexandre-io <Alexandre-io@users.noreply.github.com>
|
||||
Aram Drevekenin <grimsniffer@gmail.com>
|
||||
Bart Dubois <dubcio@o2.pl>
|
||||
Barthélemy Vessemont <bvessemont@gmail.com>
|
||||
Brandon Nicholls <brandon.nicholls@gmail.com>
|
||||
Bren Norris <bnorris@enterrasolutions.com>
|
||||
Brett Trotter <brett.trotter@webfilings.com>
|
||||
Brian Peacock <bpeacock@fastfig.com>
|
||||
Cedric Darne <cdarne@hibernum.com>
|
||||
Chad Killingsworth <chad.killingsworth@banno.com>
|
||||
Chris Breneman <crispy@cluenet.org>
|
||||
Cody Droz <cody-geest@uiowa.edu>
|
||||
Daniel Rodríguez Rivero <rdanielo@gmail.com>
|
||||
Denis Babineau <denis.babineau@gmail.com>
|
||||
Emmanuel Narh <narhe@advisory.com>
|
||||
Fabio Poloni <fabio@APP-roved.com>
|
||||
Facundo Chambó <fchambo@despegar.com>
|
||||
Guilherme Bernal <dev@lbguilherme.com>
|
||||
Jakub Jirutka <jakub@jirutka.cz>
|
||||
James Newell <j.newell@nib.com.au>
|
||||
Jan Vansteenkiste <jan@vstone.eu>
|
||||
Jannis Achstetter <jannis.achstetter@schneider-electric.com>
|
||||
Jeremy Moritz <jeremy@jeremymoritz.com>
|
||||
John Gozde <johng@pandell.com>
|
||||
Jon de la Motte <jondlm@gmail.com>
|
||||
Joseph Gentle <me@josephg.com>
|
||||
José De Paz <josedepaz@users.noreply.github.com>
|
||||
Juan Carlos Picado <juan@encuestame.org>
|
||||
Juan Carlos Picado <juanpicado19@gmail.com>
|
||||
Juan Picado <juanpicado19@gmail.com>
|
||||
Juan Picado @jotadeveloper <juanpicado19@gmail.com>
|
||||
Kalman Speier <kalman.speier@gmail.com>
|
||||
Keyvan Fatehi <keyvanfatehi@gmail.com>
|
||||
Kody J. Peterson <kodypeterson@users.noreply.github.com>
|
||||
Madison Grubb <madison.grubb@itential.com>
|
||||
Manuel de Brito Fontes <aledbf@gmail.com>
|
||||
Mark Doeswijk <mark.doeswijk@marviq.com>
|
||||
Meeeeow <i@aka.mn>
|
||||
Meeeeow <me@async.sh>
|
||||
Michael Arnel <michael.arnel@gmail.com>
|
||||
Michael Crowe <michael@developrise.com>
|
||||
Miguel Mejias <miguelangelmejias@dorna.com>
|
||||
Miroslav Bajtoš <miroslav@strongloop.com>
|
||||
Nate Ziarek <natez@OSX12-L-NATEZ.local>
|
||||
Nick <nick.edelenbos@trimm.nl>
|
||||
Piotr Synowiec <psynowiec@gmail.com>
|
||||
Rafael Cesar <rafa.cesar@gmail.com>
|
||||
Robert Ewald <r3wald@gmail.com>
|
||||
Robert Groh <robert.groh@medesso.de>
|
||||
Robin Persson <rprssn@gmail.com>
|
||||
Romain Lai-King <romain.laiking@opentrust.com>
|
||||
Ryan Graham <r.m.graham@gmail.com>
|
||||
Ryan Graham <ryan@codingintrigue.co.uk>
|
||||
Sam Day <sday@atlassian.com>
|
||||
Tarun Garg <tarun1793@users.noreply.github.com>
|
||||
Thomas Cort <thomasc@ssimicro.com>
|
||||
Tom Vincent <git@tlvince.com>
|
||||
Trent Earl <trent@trentearl.com>
|
||||
Yannick Croissant <yannick.croissant@gmail.com>
|
||||
Yannick Galatol <ygalatol@teads.tv>
|
||||
cklein <trancesilken@gmail.com>
|
||||
danielo515 <rdanielo@gmail.com>
|
||||
jmwilkinson <j.wilkinson@f5.com>
|
||||
jotadeveloper <juanpicado19@gmail.com>
|
||||
jotadeveloper <juanpicado@users.noreply.github.com>
|
||||
maxlaverse <max@laverse.net>
|
||||
saheba <saheba@users.noreply.github.com>
|
||||
steve-p-com <github@steve-p.com>
|
||||
trent.earl <trent.earl@malauzai.com>
|
||||
505
CHANGELOG.md
Normal file
505
CHANGELOG.md
Normal file
@@ -0,0 +1,505 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
<a name="2.4.0"></a>
|
||||
# [2.4.0](https://github.com/verdaccio/verdaccio/compare/v2.3.6...v2.4.0) (2017-09-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* :bug: check error code to prevent data loss ([5d73dca](https://github.com/verdaccio/verdaccio/commit/5d73dca))
|
||||
* :bug: check error code to prevent data loss ([93aae05](https://github.com/verdaccio/verdaccio/commit/93aae05))
|
||||
* :bug: Package metadata cache not work ([4d6a447](https://github.com/verdaccio/verdaccio/commit/4d6a447))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update node alpine version to 8.4.0 ([3f96ce3](https://github.com/verdaccio/verdaccio/commit/3f96ce3))
|
||||
|
||||
|
||||
|
||||
<a name="2.3.6"></a>
|
||||
## [2.3.6](https://github.com/verdaccio/verdaccio/compare/v2.3.5...v2.3.6) (2017-08-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* link was broken ([a9481cc](https://github.com/verdaccio/verdaccio/commit/a9481cc))
|
||||
* Correct accept header set for registry requests ([#295](https://github.com/verdaccio/verdaccio/pull/295))
|
||||
* Update SSL documentation ([#296](https://github.com/verdaccio/verdaccio/pull/296))
|
||||
* Fix auth process to check against username also and not just groups ([#293](https://github.com/verdaccio/verdaccio/pull/293))
|
||||
|
||||
|
||||
|
||||
<a name="2.3.5"></a>
|
||||
## [2.3.5](https://github.com/verdaccio/verdaccio/compare/v2.3.4...v2.3.5) (2017-08-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* configuration files inconsistencies, add unit test ([644c098](https://github.com/verdaccio/verdaccio/commit/644c098))
|
||||
* Remove accept header that seems cause issues [#285](https://github.com/verdaccio/verdaccio/issues/285) [#289](https://github.com/verdaccio/verdaccio/issues/289) and npm search fails ([fab8391](https://github.com/verdaccio/verdaccio/commit/fab8391))
|
||||
|
||||
|
||||
|
||||
<a name="2.3.4"></a>
|
||||
## [2.3.4](https://github.com/verdaccio/verdaccio/compare/v2.3.3...v2.3.4) (2017-07-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Docker image fails due lock file localhost references ([901a7be](https://github.com/verdaccio/verdaccio/commit/901a7be))
|
||||
|
||||
|
||||
|
||||
<a name="2.3.3"></a>
|
||||
## [2.3.3](https://github.com/verdaccio/verdaccio/compare/v2.3.2...v2.3.3) (2017-07-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* refactor [#268](https://github.com/verdaccio/verdaccio/issues/268) in a better way, amended to elegant way ([94fb6ad](https://github.com/verdaccio/verdaccio/commit/94fb6ad))
|
||||
|
||||
|
||||
|
||||
<a name="2.3.2"></a>
|
||||
## [2.3.2](https://github.com/verdaccio/verdaccio/compare/v2.3.0...v2.3.2) (2017-07-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* :bug: detail page can't handle scoped package ([1c9fbfc](https://github.com/verdaccio/verdaccio/commit/1c9fbfc))
|
||||
* [#268](https://github.com/verdaccio/verdaccio/issues/268) remove the accept header that avoids request with some regiestries ([e7dcf3c](https://github.com/verdaccio/verdaccio/commit/e7dcf3c))
|
||||
* [#78](https://github.com/verdaccio/verdaccio/issues/78) add new setting to allow publish when uplinks are offline ([430425c](https://github.com/verdaccio/verdaccio/commit/430425c))
|
||||
* broken link ([9fb0e14](https://github.com/verdaccio/verdaccio/commit/9fb0e14))
|
||||
* lint warning ([d0afe78](https://github.com/verdaccio/verdaccio/commit/d0afe78))
|
||||
* Param web.title from config.yaml does not work on docker image [#265](https://github.com/verdaccio/verdaccio/issues/265) ([b1a396d](https://github.com/verdaccio/verdaccio/commit/b1a396d))
|
||||
* undefined check ([ff96d2e](https://github.com/verdaccio/verdaccio/commit/ff96d2e))
|
||||
|
||||
|
||||
|
||||
## 2.3.1 (July 25, 2017)
|
||||
|
||||
- bug: Detail page can't handle scoped package - [#261](https://github.com/verdaccio/verdaccio/pull/261)
|
||||
- bug: can't publish a private package to verdaccio while offline - [#223](https://github.com/verdaccio/verdaccio/pull/223)
|
||||
- refactor: use light version of syntax highlighter - [#260](https://github.com/verdaccio/verdaccio/pull/260)
|
||||
|
||||
## 2.3.0 (July 22, 2017)
|
||||
|
||||
- feature: Refactor User Interface - [#220](https://github.com/verdaccio/verdaccio/pull/220)
|
||||
|
||||
## 2.2.7 (July 18, 2017)
|
||||
|
||||
- bug: fix running behind of loadbalancer with TLS termination - [#254](https://github.com/verdaccio/verdaccio/pull/254)
|
||||
|
||||
|
||||
## 2.2.6 (July 13, 2017)
|
||||
|
||||
- build: update node version due security update announcement - [#251](https://github.com/verdaccio/verdaccio/pull/251)
|
||||
|
||||
## 2.2.5 (July 4, 2017)
|
||||
|
||||
- Fixed adding the verdaccio user into the group - [#241](https://github.com/verdaccio/verdaccio/pull/241)
|
||||
|
||||
## 2.2.3 (July 4, 2017)
|
||||
|
||||
- Updated Dockerfile & added proper signal handling - [#239](https://github.com/verdaccio/verdaccio/pull/239)
|
||||
|
||||
## 2.2.2 (July 2, 2017)
|
||||
|
||||
- Improve Docker Build - [#181](https://github.com/verdaccio/verdaccio/pull/181)
|
||||
- Bugfix #73 `npm-latest` support - [#228](https://github.com/verdaccio/verdaccio/pull/228)
|
||||
- Add [documentation](https://github.com/verdaccio/verdaccio/tree/master/wiki) - [#229](https://github.com/verdaccio/verdaccio/pull/229)
|
||||
|
||||
## 2.2.1 (June 17, 2017)
|
||||
|
||||
- config section moved up, some keywords added - [#211](https://github.com/verdaccio/verdaccio/pull/211)
|
||||
- docs: update docs with behind reverse proxy - [#214](https://github.com/verdaccio/verdaccio/pull/214)
|
||||
- Add remote ip to request log - [#216](https://github.com/verdaccio/verdaccio/pull/216)
|
||||
|
||||
## 2.2.0 (June 8, 2017)
|
||||
- Allow url_prefix to be only the path - ([@BartDubois ]((https://github.com/BartDubois))) in [#197](https://github.com/verdaccio/verdaccio/pull/197)
|
||||
- Apache reverse proxy configuration - ([@mysiar ]((https://github.com/mysiar))) in [#198](https://github.com/verdaccio/verdaccio/pull/198)
|
||||
- don't blindly clobber local dist-tags - ([@rmg ]((https://github.com/rmg))) in [#206](https://github.com/verdaccio/verdaccio/pull/206)
|
||||
- Adds cache option to uplinks - ([@silkentrance ]((https://github.com/silkentrance))) in [#132](https://github.com/verdaccio/verdaccio/pull/132)
|
||||
|
||||
## 2.1.7 (May 14, 2017)
|
||||
- Fixed publish fail in YARN - ([@W1U02]((https://github.com/W1U02)) in [#183](https://github.com/verdaccio/verdaccio/pull/183)
|
||||
|
||||
## 2.1.6 (May 12, 2017)
|
||||
- Fix https certificates safety check - ([@juanpicado]((https://github.com/juanpicado))) in [#189](https://github.com/verdaccio/verdaccio/pull/189)
|
||||
- Fix upstream search not work with gzip - ([@Meeeeow](https://github.com/Meeeeow) in [#170](https://github.com/verdaccio/verdaccio/pull/170))
|
||||
- Add additional requirement to output message - ([@marnel ](https://github.com/marnel) in [#184](https://github.com/verdaccio/verdaccio/pull/184))
|
||||
- Implement npm ping endpoint - ([@juanpicado]((https://github.com/juanpicado))) in [#179](https://github.com/verdaccio/verdaccio/pull/179)
|
||||
- Add support for multiple notification endpoints to existing webhook - ([@ryan-codingintrigue]((https://github.com/ryan-codingintrigue)))
|
||||
in [#108](https://github.com/verdaccio/verdaccio/pull/108)
|
||||
|
||||
|
||||
|
||||
## 2.1.5 (April 22, 2017)
|
||||
- fix upstream search - ([@Meeeeow](https://github.com/Meeeeow) in [#166](https://github.com/verdaccio/verdaccio/pull/166))
|
||||
- Fix search feature - ([@Meeeeow](https://github.com/Meeeeow) in [#163](https://github.com/verdaccio/verdaccio/pull/163))
|
||||
- add docs about run behind proxy - ([@Meeeeow](https://github.com/Meeeeow) in [#160](https://github.com/verdaccio/verdaccio/pull/160))
|
||||
|
||||
## 2.1.4 (April 13, 2017)
|
||||
- Added Nexus Repository OSS as similar existing software - ([@nedelenbos030](https://github.com/nedelenbos) in [#147](https://github.com/verdaccio/verdaccio/pull/147))
|
||||
- Increase verbose on notify request - ([@juanpicado](https://github.com/juanpicado) in [#153](https://github.com/verdaccio/verdaccio/pull/153))
|
||||
- Add fallback support to previous config files - ([@juanpicado](https://github.com/juanpicado) in [#155](https://github.com/verdaccio/verdaccio/pull/155))
|
||||
- Allows retrieval of all local package contents via http://server/-/search/* - ([@Verikon](https://github.com/Verikon) in [#152](https://github.com/verdaccio/verdaccio/pull/155))
|
||||
|
||||
## 2.1.3 (March 29, 2017)
|
||||
- [GH-83] create systemd service - ([@030](https://github.com/030) in [#89](https://github.com/verdaccio/verdaccio/pull/89))
|
||||
- optional scope in the readme package name. - ([@psychocode](https://github.com/psychocode) in [#136](https://github.com/verdaccio/verdaccio/pull/136))
|
||||
- Added docker image for rpi - ([@danielo515](https://github.com/danielo515) in [#137](https://github.com/verdaccio/verdaccio/pull/137))
|
||||
- Allow configuring a tagline that is displayed on the webpage between. ([@jachstet-sea](https://github.com/jachstet-sea) in [#143](https://github.com/verdaccio/verdaccio/pull/143))
|
||||
|
||||
## 2.1.2 (March 9, 2017)
|
||||
- Contribute guidelines - ([@juanpicado](https://github.com/juanpicado) in [#133](https://github.com/verdaccio/verdaccio/pull/133))
|
||||
- fix(plugin-loader): plugins verdaccio-* overwrite by sinopia- ([@Alexandre-io](https://github.com/Alexandre-io) in [#129](https://github.com/verdaccio/verdaccio/pull/129))
|
||||
|
||||
## 2.1.1 (February 7, 2017)
|
||||
|
||||
- [GH-86] updated readme to point to new chef cookbook ([@kgrubb](https://github.com/kgrubb) in [#117](https://github.com/verdaccio/verdaccio/pull/117))
|
||||
- [GH-88] rename to Verdaccio instead of Sinopia ([@kgrubb](https://github.com/kgrubb) in [#93](https://github.com/verdaccio/verdaccio/pull/93))
|
||||
- Unit testing coverage ([@juanpicado](https://github.com/juanpicado) in [#116](https://github.com/verdaccio/verdaccio/issues/116))
|
||||
- Allow htpasswd-created users to log in [@imsnif](https://github.com/imsnif) in [#112](https://github.com/verdaccio/verdaccio/issues/112))
|
||||
- remove travis io.js support ([@juanpicado](https://github.com/juanpicado) in [#115](https://github.com/verdaccio/verdaccio/issues/115))
|
||||
- rename clean up ([@juanpicado](https://github.com/juanpicado) in [#114](https://github.com/verdaccio/verdaccio/issues/114))
|
||||
- _npmUser / author not showing up ([@juanpicado](https://github.com/juanpicado) in [#65](https://github.com/verdaccio/verdaccio/issues/65))
|
||||
- Docs: correct config attribute `proxy_access` ([@robertgroh](https://github.com/robertgroh) in [#96](https://github.com/verdaccio/verdaccio/pull/96))
|
||||
- Problem with docker.yaml ([@josedepaz](https://github.com/josedepaz) in [#72](https://github.com/verdaccio/verdaccio/pull/72))
|
||||
- Prevent logging of user and password ([@tlvince](https://github.com/tlvince) in [#94](https://github.com/verdaccio/verdaccio/pull/94))
|
||||
- Updated README.md to reflect the availability of the docker image ([@jmwilkinson](https://github.com/jmwilkinson)) in [#71](https://github.com/verdaccio/verdaccio/pull/71))
|
||||
|
||||
## 2.1.0 (October 11, 2016)
|
||||
|
||||
- Use __dirname to resolve local plugins ([@aledbf](https://github.com/aledbf) in [#25](https://github.com/verdaccio/verdaccio/pull/25))
|
||||
- Fix npm cli logout ([@plitex](https://github.com/plitex) in [#47](https://github.com/verdaccio/verdaccio/pull/47))
|
||||
- Add log format: pretty-timestamped ([@jachstet-sea](https://github.com/jachstet-sea) in [#68](https://github.com/verdaccio/verdaccio/pull/68))
|
||||
- Allow adding/overriding HTTP headers of uplinks via config ([@jachstet-sea](https://github.com/jachstet-sea) in [#67](https://github.com/verdaccio/verdaccio/pull/67))
|
||||
- Update Dockerfile to fix failed start ([@denisbabineau](https://github.com/denisbabineau) in [#62](https://github.com/verdaccio/verdaccio/pull/62))
|
||||
- Update the configs to fully support proxying scoped packages ([@ChadKillingsworth](https://github.com/ChadKillingsworth) in [#60](https://github.com/verdaccio/verdaccio/pull/60))
|
||||
- Prevent the server from crashing if a repo is accessed that the user does not have access to ([@crowebird](https://github.com/crowebird) in [#58](https://github.com/verdaccio/verdaccio/pull/58))
|
||||
- Hook system, for integration into things like slack
|
||||
- Register entry partial even if custom template is provided ([@plitex](https://github.com/plitex) in [#46](https://github.com/verdaccio/verdaccio/pull/46))
|
||||
- Rename process to verdaccio ([@juanpicado](https://github.com/juanpicado) in [#57](https://github.com/verdaccio/verdaccio/pull/57))
|
||||
|
||||
|
||||
## 7 Jun 2015, version 1.4.0
|
||||
|
||||
- avoid sending X-Forwarded-For through proxies (issues [#19](https://github.com/rlidwka/sinopia/issues/19), [#254](https://github.com/rlidwka/sinopia/issues/254))
|
||||
- fix multiple issues in search (issues [#239](https://github.com/rlidwka/sinopia/issues/239), [#253](https://github.com/rlidwka/sinopia/pull/253))
|
||||
- fix "maximum stack trace exceeded" errors in auth (issue [#258](https://github.com/rlidwka/sinopia/issues/258))
|
||||
|
||||
## 10 May 2015, version 1.3.0
|
||||
|
||||
- add dist-tags endpoints (issue [#211](https://github.com/rlidwka/sinopia/issues/211))
|
||||
|
||||
## 22 Apr 2015, version 1.2.2
|
||||
|
||||
- fix access control regression in `1.2.1` (issue [#238](https://github.com/rlidwka/sinopia/issues/238))
|
||||
- add a possibility to bind on unix sockets (issue [#237](https://github.com/rlidwka/sinopia/issues/237))
|
||||
|
||||
## 11 Apr 2015, version 1.2.1
|
||||
|
||||
- added more precise authorization control to auth plugins (issue [#207](https://github.com/rlidwka/sinopia/pull/207))
|
||||
|
||||
## 29 Mar 2015, version 1.1.0
|
||||
|
||||
- add a possibility to listen on multiple ports (issue [#172](https://github.com/rlidwka/sinopia/issues/172))
|
||||
- added https support (issues [#71](https://github.com/rlidwka/sinopia/issues/71), [#166](https://github.com/rlidwka/sinopia/issues/166))
|
||||
- added an option to use a custom template for web UI (issue [#208](https://github.com/rlidwka/sinopia/pull/208))
|
||||
- remove "from" and "resolved" fields from shrinkwrap (issue [#204](https://github.com/rlidwka/sinopia/issues/204))
|
||||
- fix hanging when rendering readme (issue [#206](https://github.com/rlidwka/sinopia/issues/206))
|
||||
- fix logger-related crash when using sinopia as a library
|
||||
- all requests to uplinks should now have proper headers
|
||||
|
||||
## 12 Feb 2015, version 1.0.1
|
||||
|
||||
- fixed issue with `max_users` option (issue [#184](https://github.com/rlidwka/sinopia/issues/184))
|
||||
- fixed issue with not being able to disable the web interface (issue [#195](https://github.com/rlidwka/sinopia/pull/195))
|
||||
- fixed 500 error while logging in with npm (issue [#200](https://github.com/rlidwka/sinopia/pull/200))
|
||||
|
||||
## 26 Jan 2015, version 1.0.0
|
||||
|
||||
- switch markdown parser from `remarkable` to `markdown-it`
|
||||
- update `npm-shrinkwrap.json`
|
||||
- now downloading tarballs from upstream using the same protocol as for metadata (issue [#166](https://github.com/rlidwka/sinopia/issues/166))
|
||||
|
||||
## 22 Dec 2014, version 1.0.0-beta.2
|
||||
|
||||
- fix windows behavior when `$HOME` isn't set (issue [#177](https://github.com/rlidwka/sinopia/issues/177))
|
||||
- fix sanitization for highlighted code blocks in readme (issue [render-readme/#1](https://github.com/rlidwka/render-readme/issues/1))
|
||||
|
||||
## 15 Dec 2014, version 1.0.0-beta
|
||||
|
||||
- Markdown rendering is now a lot safer (switched to remarkable+sanitizer).
|
||||
- Header in web interface is now static instead of fixed.
|
||||
- `GET /-/all?local` now returns list of all local packages (issue [#179](https://github.com/rlidwka/sinopia/pull/179))
|
||||
|
||||
## 5 Dec 2014, version 1.0.0-alpha.3
|
||||
|
||||
- Fixed an issue with scoped packages in tarballs
|
||||
|
||||
## 25 Nov 2014, version 1.0.0-alpha
|
||||
|
||||
- Config file is now created in `$XDG_CONFIG_HOME` instead of current directory.
|
||||
|
||||
It is printed to stdout each time sinopia starts, so you hopefully won't have any trouble locating it.
|
||||
|
||||
The change is made so sinopia will pick up the same config no matter which directory it is started from.
|
||||
|
||||
- Default config file is now a lot shorter, and it is very permissive by default. You could use sinopia without modifying it on your own computer, but definitely should change it on production.
|
||||
|
||||
- Added auth tokens. For now, auth token is just a username+password encrypted for security reasons, so it isn't much different from basic auth, but allows to avoid "always-auth" npm setting.
|
||||
|
||||
- Added scoped packages.
|
||||
|
||||
Please note that default `*` mask won't apply to them. You have to use masks like `@scope/*` to match scoped packages, or `**` to match everything.
|
||||
|
||||
- Enabled web interface by default. Wow, it looks almost ready now!
|
||||
|
||||
- All dependencies are bundled now, so uncompatible changes in 3rd party stuff in the future won't ruin the day.
|
||||
|
||||
## 1 Nov 2014, version 0.13.2
|
||||
|
||||
- fix `EPERM`-related crashes on windows (issue [#67](https://github.com/rlidwka/sinopia/issues/67))
|
||||
|
||||
## 22 Oct 2014, version 0.13.0
|
||||
|
||||
- web interface:
|
||||
- web page layout improved (issue [#141](https://github.com/rlidwka/sinopia/pull/141))
|
||||
- latest version is now displayed correctly (issues [#120](https://github.com/rlidwka/sinopia/issues/120), [#123](https://github.com/rlidwka/sinopia/issues/123), [#143](https://github.com/rlidwka/sinopia/pull/143))
|
||||
- fixed web interface working behind reverse proxy (issues [#145](https://github.com/rlidwka/sinopia/issues/145), [#147](https://github.com/rlidwka/sinopia/issues/147))
|
||||
|
||||
## 2 Oct 2014, version 0.12.1
|
||||
|
||||
- web interface:
|
||||
- update markdown CSS (issue [#137](https://github.com/rlidwka/sinopia/pull/137))
|
||||
- jquery is now served locally (issue [#133](https://github.com/rlidwka/sinopia/pull/133))
|
||||
|
||||
- bugfixes:
|
||||
- fix "offset out of bounds" issues (issue [sinopia-htpasswd/#2](https://github.com/rlidwka/sinopia-htpasswd/issues/2))
|
||||
- "max_users" in htpasswd plugin now work correctly (issue [sinopia-htpasswd/#3](https://github.com/rlidwka/sinopia-htpasswd/issues/3))
|
||||
- fix `ENOTDIR, open '.sinopia-db.json'` error in npm search (issue [#122](https://github.com/rlidwka/sinopia/issues/122))
|
||||
|
||||
## 25 Sep 2014, version 0.12.0
|
||||
|
||||
- set process title to `sinopia`
|
||||
|
||||
- web interface bugfixes:
|
||||
- save README data for each package (issue [#100](https://github.com/rlidwka/sinopia/issues/100))
|
||||
- fix crashes related to READMEs (issue [#128](https://github.com/rlidwka/sinopia/issues/128))
|
||||
|
||||
## 18 Sep 2014, version 0.11.3
|
||||
|
||||
- fix 500 error in adduser function in sinopia-htpasswd (issue [#121](https://github.com/rlidwka/sinopia/issues/121))
|
||||
- fix fd leak in authenticate function in sinopia-htpasswd (issue [#116](https://github.com/rlidwka/sinopia/issues/116))
|
||||
|
||||
## 15 Sep 2014, version 0.11.1
|
||||
|
||||
- mark crypt3 as optional (issue [#119](https://github.com/rlidwka/sinopia/issues/119))
|
||||
|
||||
## 15 Sep 2014, version 0.11.0
|
||||
|
||||
- Added auth plugins (issue [#99](https://github.com/rlidwka/sinopia/pull/99))
|
||||
|
||||
Now you can create your own auth plugin based on [sinopia-htpasswd](https://github.com/rlidwka/sinopia-htpasswd) package.
|
||||
|
||||
- WIP: web interface (issue [#73](https://github.com/rlidwka/sinopia/pull/73))
|
||||
|
||||
It is disabled by default, and not ready for production yet. Use at your own risk. We will enable it in the next major release.
|
||||
|
||||
- Some modules are now bundled by default, so users won't have to install stuff from git. We'll see what issues it causes, maybe all modules will be bundled in the future like in npm.
|
||||
|
||||
## 14 Sep 2014, version 0.10.x
|
||||
|
||||
*A bunch of development releases that are broken in various ways. Please use 0.11.x instead.*
|
||||
|
||||
## 7 Sep 2014, version 0.9.3
|
||||
|
||||
- fix several bugs that could cause "can't set headers" exception
|
||||
|
||||
## 3 Sep 2014, version 0.9.2
|
||||
|
||||
- allow "pretty" format for logging into files (issue [#88](https://github.com/rlidwka/sinopia/pull/88))
|
||||
- remove outdated user existence check (issue [#115](https://github.com/rlidwka/sinopia/pull/115))
|
||||
|
||||
## 11 Aug 2014, version 0.9.1
|
||||
|
||||
- filter falsey _npmUser values (issue [#95](https://github.com/rlidwka/sinopia/pull/95))
|
||||
- option not to cache third-party files (issue [#85](https://github.com/rlidwka/sinopia/issues/85))
|
||||
|
||||
## 26 Jul 2014, version 0.9.0
|
||||
|
||||
- new features:
|
||||
- add search functionality (issue [#65](https://github.com/rlidwka/sinopia/pull/65))
|
||||
- allow users to authenticate using .htpasswd (issue [#44](https://github.com/rlidwka/sinopia/issues/44))
|
||||
- allow user registration with "npm adduser" (issue [#44](https://github.com/rlidwka/sinopia/issues/44))
|
||||
|
||||
- bugfixes:
|
||||
- avoid crashing when res.socket is null (issue [#89](https://github.com/rlidwka/sinopia/issues/89))
|
||||
|
||||
## 20 Jun 2014, version 0.8.2
|
||||
|
||||
- allow '@' in package/tarball names (issue [#75](https://github.com/rlidwka/sinopia/issues/75))
|
||||
- other minor fixes (issues [#77](https://github.com/rlidwka/sinopia/issues/77), [#80](https://github.com/rlidwka/sinopia/issues/80))
|
||||
|
||||
## 14 Apr 2014, version 0.8.1
|
||||
|
||||
- "latest" tag is now always present in any package (issue [#63](https://github.com/rlidwka/sinopia/issues/63))
|
||||
- tags created with new npm versions (>= 1.3.19) can now be published correctly
|
||||
|
||||
## 1 Apr 2014, version 0.8.0
|
||||
|
||||
- use gzip compression whenever possible (issue [#54](https://github.com/rlidwka/sinopia/issues/54))
|
||||
- set `ignore_latest_tag` to false, it should now be more compatible with npm registry
|
||||
- make `fs-ext` optional (issue [#61](https://github.com/rlidwka/sinopia/issues/61))
|
||||
|
||||
## 29 Mar 2014, version 0.7.1
|
||||
|
||||
- added `ignore_latest_tag` config param (issues [#55](https://github.com/rlidwka/sinopia/issues/55), [#59](https://github.com/rlidwka/sinopia/issues/59))
|
||||
- reverted PR [#56](https://github.com/rlidwka/sinopia/issues/56) (see discussion in [#57](https://github.com/rlidwka/sinopia/issues/57))
|
||||
|
||||
## 13 Mar 2014, version 0.7.0
|
||||
|
||||
- config changes:
|
||||
- breaking change: all time intervals are now specified in *seconds* instead of *milliseconds* for the sake of consistency. Change `timeout` if you have one!
|
||||
- all time intervals now can be specified in [nginx notation](http://wiki.nginx.org/ConfigNotation), for example `1m 30s` will specify a 90 seconds timeout
|
||||
- added `maxage` option to avoid asking public registry for the same data too often (issue [#47](https://github.com/rlidwka/sinopia/issues/47))
|
||||
- added `max_fails` and `fail_timeout` options to reduce amount of requests to public registry when it's down (issue [#7](https://github.com/rlidwka/sinopia/issues/7))
|
||||
|
||||
- bug fixes:
|
||||
- fix crash when headers are sent twice (issue [#52](https://github.com/rlidwka/sinopia/issues/52))
|
||||
- all tarballs are returned with `Content-Length`, which allows [yapm](https://github.com/rlidwka/yapm) to estimate download time
|
||||
- when connection to public registry is interrupted when downloading a tarball, we no longer save incomplete tarball to the disk
|
||||
|
||||
- other changes:
|
||||
- 404 errors are returned in couchdb-like manner (issue [#56](https://github.com/rlidwka/sinopia/issues/56))
|
||||
|
||||
## 5 Mar 2014, version 0.6.7
|
||||
|
||||
- pin down express@3 version, since sinopia doesn't yet work with express@4
|
||||
|
||||
## 28 Feb 2014, version 0.6.5
|
||||
|
||||
- old SSL keys for npm are removed, solves `SELF_SIGNED_CERT_IN_CHAIN` error
|
||||
|
||||
## 3 Feb 2014, version 0.6.3
|
||||
|
||||
- validate tags and versions (issue [#40](https://github.com/rlidwka/sinopia/issues/40))
|
||||
- don't crash when process.getuid doesn't exist (issue [#41](https://github.com/rlidwka/sinopia/issues/41))
|
||||
|
||||
## 18 Jan 2014, version 0.6.2
|
||||
|
||||
- adding config param to specify upload limits (issue [#39](https://github.com/rlidwka/sinopia/issues/39))
|
||||
- making loose semver versions work (issue [#38](https://github.com/rlidwka/sinopia/issues/38))
|
||||
|
||||
## 13 Jan 2014, version 0.6.1
|
||||
|
||||
- support setting different storage paths for different packages (issue [#35](https://github.com/rlidwka/sinopia/issues/35))
|
||||
|
||||
## 30 Dec 2013, version 0.6.0
|
||||
|
||||
- tag support (issue [#8](https://github.com/rlidwka/sinopia/issues/8))
|
||||
- adding support for npm 1.3.19+ behaviour (issue [#31](https://github.com/rlidwka/sinopia/issues/31))
|
||||
- removing all support for proxying publish requests to uplink (too complex)
|
||||
|
||||
## 26 Dec 2013, version 0.5.9
|
||||
|
||||
- fixing bug with bad Accept header (issue [#32](https://github.com/rlidwka/sinopia/issues/32))
|
||||
|
||||
## 20 Dec 2013, version 0.5.8
|
||||
|
||||
- fixed a warning from js-yaml
|
||||
- don't color multiline strings in logs output
|
||||
- better error messages in various cases
|
||||
- test format changed
|
||||
|
||||
## 15 Dec 2013, version 0.5.7
|
||||
|
||||
- try to fetch package from uplinks if user requested a tarball we don't know about (issue [#29](https://github.com/rlidwka/sinopia/issues/29))
|
||||
- security fix: set express.js to production mode so we won't return stack traces to the user in case of errors
|
||||
|
||||
## 11 Dec 2013, version 0.5.6
|
||||
|
||||
- fixing a few crashes related to tags
|
||||
|
||||
## 8 Dec 2013, version 0.5.4
|
||||
|
||||
- latest tag always shows highest version available (issue [#8](https://github.com/rlidwka/sinopia/issues/8))
|
||||
- added a configurable timeout for requests to uplinks (issue [#18](https://github.com/rlidwka/sinopia/issues/18))
|
||||
- users with bad authentication header are considered not logged in (issue [#17](https://github.com/rlidwka/sinopia/issues/17))
|
||||
|
||||
## 24 Nov 2013, version 0.5.3
|
||||
|
||||
- added proxy support for requests to uplinks (issue [#13](https://github.com/rlidwka/sinopia/issues/13))
|
||||
- changed license from default BSD to WTFPL
|
||||
|
||||
## 26 Oct 2013, version 0.5.2
|
||||
|
||||
- server now supports unpublishing local packages
|
||||
- added fs-ext dependency (flock)
|
||||
- fixed a few face conditions
|
||||
|
||||
## 20 Oct 2013, version 0.5.1
|
||||
|
||||
- fixed a few errors related to logging
|
||||
|
||||
## 12 Oct 2013, version 0.5.0
|
||||
|
||||
- using bunyan as a log engine
|
||||
- pretty-formatting colored logs to stdout by default
|
||||
- ask user before creating any config files
|
||||
|
||||
## 5 Oct 2013, version 0.4.3
|
||||
|
||||
- basic tags support for npm (read-only)
|
||||
- npm star/unstar calls now return proper error
|
||||
|
||||
## 29 Sep 2013, version 0.4.2
|
||||
|
||||
## 28 Sep 2013, version 0.4.1
|
||||
|
||||
- using mocha for tests now
|
||||
- making use of streams2 api, doesn't work on 0.8 anymore
|
||||
- basic support for uploading packages to other registries
|
||||
|
||||
## 27 Sep 2013, version 0.4.0
|
||||
|
||||
- basic test suite
|
||||
- storage path in config is now relative to config file location, not cwd
|
||||
- proper cleanup for temporary files
|
||||
|
||||
## 12 Jul 2013, version 0.3.2
|
||||
|
||||
## 4 Jul 2013, version 0.3.1
|
||||
|
||||
- using ETag header for all json output, based on md5
|
||||
|
||||
## 20 Jun 2013, version 0.3.0
|
||||
|
||||
- compression for http responses
|
||||
- requests for files to uplinks are now streams (no buffering)
|
||||
- tarballs are now cached locally
|
||||
|
||||
## 19 Jun 2013, version 0.2.0
|
||||
|
||||
- config file changed, packages is now specified with minimatch
|
||||
- ability to retrieve all packages from another registry (i.e. npmjs)
|
||||
|
||||
## 14 Jun 2013, version 0.1.1
|
||||
|
||||
- config is now autogenerated
|
||||
- tarballs are now read/written from fs using streams (no buffering)
|
||||
|
||||
## 9 Jun 2013, version 0.1.0
|
||||
|
||||
- first npm version
|
||||
- ability to publish packages and retrieve them locally
|
||||
- basic authentication/access control
|
||||
|
||||
## 22 May 2013, version 0.0.0
|
||||
|
||||
- first commits
|
||||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at verdaccio.npm@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
209
CONTRIBUTING.md
Normal file
209
CONTRIBUTING.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# Contributing to Verdaccio
|
||||
|
||||
We are happy you wish to contribute this project, for that reason we want to board you with this guide.
|
||||
|
||||
## How I contribute?
|
||||
|
||||
|
||||
### Ways to contribute
|
||||
|
||||
There are many ways to contribute to the Verdaccio Project. Here’s a list of technical contributions with increasing levels of involvement and required knowledge of Verdaccio's code and operations.
|
||||
|
||||
* [Reporting a Bug](CONTRIBUTING.md#reporting-a-bug)
|
||||
* [Request Features](CONTRIBUTING.md#request-features)
|
||||
* [Plugins](CONTRIBUTING.md#plugins)
|
||||
* [Improve the Documentation](wiki/README.md)
|
||||
|
||||
Please read carefully this document. It will guide you to provide maintainers and readers valuable information to boots the process solve the issue or evaluate your proposal.
|
||||
|
||||
## Reporting a Bug
|
||||
|
||||
We welcome clear bug reports. If you've found a bug in Verdaccio that isn't a security risk, please file a report in our [issue tracker](https://github.com/verdaccio/verdaccio/issues). Before you file your issue, search to see if it has already been reported. If so, up-vote (using GitHub reactions) or add additional helpful details to the existing issue to show that it's affecting multiple people.
|
||||
|
||||
### Check if there's a simple solution in the wiki.
|
||||
|
||||
Some of the most popular topics can be found in our [wiki](https://github.com/verdaccio/verdaccio/wiki), that would be the first place to look at the topic you are interested.
|
||||
|
||||
### Questions & Chat
|
||||
|
||||
We have tagged questions for easy follow up under the tag [questions](https://github.com/verdaccio/verdaccio/labels/question). Additionaly, I'd recommend to deliver questions in the new chat as **#questions/#development** channels at [gitter](https://gitter.im/verdaccio/).
|
||||
|
||||
### Look at the past
|
||||
|
||||
* Verdaccio is a fork of `sinopia@1.4.0`, thereforce, there is a huge [database of tickets](https://github.com/rlidwka/sinopia/issues) in the original projet. It's a good place to find answers.
|
||||
* Questions under the tag of [sinopia](http://stackoverflow.com/questions/tagged/sinopia) or [verdaccio](http://stackoverflow.com/search?q=verdaccio) at Stackoverflow might be helpful.
|
||||
|
||||
### Using the issue tracker
|
||||
|
||||
The issue tracker is a channel were mostly users/developers post.
|
||||
|
||||
#### I want to report a bug
|
||||
|
||||
We considere a bug a feature that is not working as is described in the documentation. Before reporte a bug follow the next steps:
|
||||
|
||||
1. Use the GitHub issue search — check if the issue has already been reported.
|
||||
|
||||
2. Check if the issue has been fixed — try to reproduce it using the latest master or development branch in the repository.
|
||||
|
||||
Verdaccio still does not support all npm commands due either in the initial design were not considered important or nobody has request it yet.
|
||||
|
||||
## Request Features
|
||||
|
||||
A new feature is always welcome, thus, analyse whether you ir idea fits in the scope of the project and elaborate your request providing enough context, for instance:
|
||||
|
||||
* A wide description the advantages of your request.
|
||||
* It's compatible with `npm` and `yarn`?
|
||||
* You might implement your feature and provide a forked repository as example.
|
||||
* Whatever you have on mind 🤓.
|
||||
|
||||
### Submitting a Pull Request
|
||||
The following are the general steps you should follow in creating a pull request. Subsequent pull requests only need
|
||||
to follow step 3 and beyond:
|
||||
|
||||
1. Fork the repository on GitHub
|
||||
2. Clone the forked repository to your machine
|
||||
3. Create a "feature" branch in your local repository
|
||||
4. Make your changes and commit them to your local repository
|
||||
5. Rebase and push your commits to your GitHub remote fork/repository
|
||||
6. Issue a Pull Request to the official repository
|
||||
7. Your Pull Request is reviewed by a committer and merged into the repository
|
||||
|
||||
*Note*: While there are other ways to accomplish the steps using other tools, the examples here will assume the most
|
||||
actions will be performed via the `git` command line.
|
||||
|
||||
### 1. Fork the Repository
|
||||
|
||||
When logged in to your GitHub account, and you are viewing one of the main repositories, you will see the *Fork* button.
|
||||
Clicking this button will show you which repositories you can fork to. Choose your own account. Once the process
|
||||
finishes, you will have your own repository that is "forked" from the official one.
|
||||
|
||||
Forking is a GitHub term and not a git term. Git is a wholly distributed source control system and simply worries
|
||||
about local and remote repositories and allows you to manage your code against them. GitHub then adds this additional
|
||||
layer of structure of how repositories can relate to each other.
|
||||
|
||||
### 2. Clone the Forked Repository
|
||||
|
||||
Once you have successfully forked your repository, you will need to clone it locally to your machine:
|
||||
|
||||
```bash
|
||||
$ git clone --recursive git@github.com:username/verdaccio.git verdaccio
|
||||
```
|
||||
|
||||
This will clone your fork to your current path in a directory named `verdaccio`.
|
||||
|
||||
You should also set up the `upstream` repository. This will allow you to take changes from the "master" repository
|
||||
and merge them into your local clone and then push them to your GitHub fork:
|
||||
|
||||
```bash
|
||||
$ cd verdaccio
|
||||
$ git remote add upstream git@github.com:verdaccio/verdaccio.git
|
||||
$ git fetch upstream
|
||||
```
|
||||
|
||||
Then you can retrieve upstream changes and rebase on them into your code like this:
|
||||
|
||||
```bash
|
||||
$ git pull --rebase upstream master
|
||||
```
|
||||
|
||||
For more information on maintaining a fork, please see the GitHub Help article [Fork a Repo](https://help.github.com/articles/fork-a-repo/) and information on
|
||||
[rebasing](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) from git.
|
||||
|
||||
### 3. Create a Branch
|
||||
|
||||
The easiest workflow is to keep your master branch in sync with the upstream branch and do not locate any of your own
|
||||
commits in that branch. When you want to work on a new feature, you then ensure you are on the master branch and create
|
||||
a new branch from there. While the name of the branch can be anything, it can often be easy to use the issue number
|
||||
you might be working on (if an issue was opened prior to opening a pull request). For example:
|
||||
|
||||
```bash
|
||||
$ git checkout -b issue-12345 master
|
||||
Switched to a new branch 'issue-12345'
|
||||
```
|
||||
|
||||
You will then be on the feature branch. You can verify what branch you are on like this:
|
||||
|
||||
```bash
|
||||
$ git status
|
||||
# On branch issue-12345
|
||||
nothing to commit, working directory clean
|
||||
```
|
||||
|
||||
### 4. Make Changes and Commit
|
||||
|
||||
#### Before commit
|
||||
|
||||
At this point you have ready your changes, your new feature it's ready to be shipped, but, to avoid delays to merge, please be aware the build must past.
|
||||
|
||||
Before commit, run the test command:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
It won't have **eslint** errors and **all test must past**. Then, and only then, you should push and ship your **PR**.
|
||||
|
||||
*At the moment of this writing, there are plenty of warning to clean, but please warnings are not fails, but try to don't commit code with warnings*
|
||||
|
||||
#### After testing your changes
|
||||
|
||||
Now you just need to make your changes. Once you have finished your changes (and tested them) you need to commit them
|
||||
to your local repository (assuming you have staged your changes for committing):
|
||||
|
||||
```bash
|
||||
$ git status
|
||||
# On branch issue-12345
|
||||
# Changes to be committed:
|
||||
# (use "git reset HEAD <file>..." to unstage)
|
||||
#
|
||||
# modified: somefile.js
|
||||
#
|
||||
$ git commit -m "Corrects some defect, fixes #12345, refs #12346"
|
||||
[t12345 0000000] Corrects some defect, fixes #12345, refs #12346
|
||||
1 file changed, 2 insertions(+), 2 deletions(-)
|
||||
```
|
||||
|
||||
### 5. Rebase and Push Changes
|
||||
|
||||
If you have been working on your contribution for a while, the upstream repository may have changed. You may want to
|
||||
ensure your work is on top of the latest changes so your pull request can be applied cleanly:
|
||||
|
||||
```bash
|
||||
$ git pull --rebase upstream master
|
||||
```
|
||||
|
||||
When you are ready to push your commit to your GitHub repository for the first time on this branch you would do the
|
||||
following:
|
||||
|
||||
```bash
|
||||
$ git push -u origin issue-12345
|
||||
```
|
||||
|
||||
After the first time, you simply need to do:
|
||||
|
||||
```bash
|
||||
$ git push
|
||||
```
|
||||
|
||||
### 6. Issue a Pull Request
|
||||
|
||||
In order to have your commits merged into the main repository, you need to create a pull request. The instructions for
|
||||
this can be found in the GitHub Help Article [Creating a Pull Request](https://help.github.com/articles/creating-a-pull-request/). Essentially you do the following:
|
||||
|
||||
1. Go to the site for your repository.
|
||||
2. Click the Pull Request button.
|
||||
3. Select the feature branch from your repository.
|
||||
4. Enter a title and description of your pull request in the description.
|
||||
5. Review the commit and files changed tabs.
|
||||
6. Click `Send Pull Request`
|
||||
|
||||
You will get notified about the status of your pull request based on your GitHub settings.
|
||||
|
||||
|
||||
## Plugins
|
||||
|
||||
Plugins are Add-ons that extend the functionality of the application. Whether you want develop your own plugin I'd suggest do the following:
|
||||
|
||||
1. Check whether there is a legacy sinopia plugin for the feature that you need at [npmjs](https://www.npmjs.com/search?q=sinopia).
|
||||
2. There is a [life-cycle to load a plugin](https://github.com/verdaccio/verdaccio/blob/master/lib/plugin-loader.js#L22) you should keep on mind.
|
||||
3. You are free to host your plugin in your repository, whether you want to host within in our organization, feel free to ask, we'll happy to host it.
|
||||
4. Try a describe widely your plugin to provide a deeply understanding to your users.
|
||||
43
Dockerfile
Normal file
43
Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
||||
FROM node:8.4.0-alpine
|
||||
LABEL maintainer="https://github.com/verdaccio/verdaccio"
|
||||
|
||||
RUN apk --no-cache add openssl && \
|
||||
wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 && \
|
||||
chmod +x /usr/local/bin/dumb-init && \
|
||||
apk del openssl
|
||||
|
||||
ENV APPDIR /usr/local/app
|
||||
|
||||
WORKDIR $APPDIR
|
||||
|
||||
ADD . $APPDIR
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm config set registry http://registry.npmjs.org/ && \
|
||||
npm install -g -s --no-progress yarn --pure-lockfile && \
|
||||
yarn install --production=false && \
|
||||
yarn run build:webui && \
|
||||
yarn cache clean && \
|
||||
yarn install --production=true --pure-lockfile
|
||||
|
||||
RUN mkdir -p /verdaccio/storage /verdaccio/conf
|
||||
|
||||
ADD conf/docker.yaml /verdaccio/conf/config.yaml
|
||||
|
||||
RUN addgroup -S verdaccio && adduser -S -G verdaccio verdaccio && \
|
||||
chown -R verdaccio:verdaccio "$APPDIR" && \
|
||||
chown -R verdaccio:verdaccio /verdaccio
|
||||
|
||||
USER verdaccio
|
||||
|
||||
ENV PORT 4873
|
||||
ENV PROTOCOL http
|
||||
|
||||
EXPOSE $PORT
|
||||
|
||||
VOLUME ["/verdaccio"]
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
|
||||
|
||||
CMD $APPDIR/bin/verdaccio --config /verdaccio/conf/config.yaml --listen $PROTOCOL://0.0.0.0:${PORT}
|
||||
13
Dockerfile.rpi
Normal file
13
Dockerfile.rpi
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM hypriot/rpi-node:6-onbuild
|
||||
|
||||
RUN mkdir -p /verdaccio/storage /verdaccio/conf
|
||||
|
||||
WORKDIR /verdaccio
|
||||
|
||||
ADD conf/docker.yaml /verdaccio/conf/config.yaml
|
||||
|
||||
EXPOSE 4873
|
||||
|
||||
VOLUME ["/verdaccio/conf", "/verdaccio/storage"]
|
||||
|
||||
CMD ["/usr/src/app/bin/verdaccio", "--config", "/verdaccio/conf/config.yaml", "--listen", "0.0.0.0:4873"]
|
||||
229
README.md
229
README.md
@@ -1,89 +1,230 @@
|
||||
**This thing doesn't work yet, come back in a few weeks**
|
||||
# Verdaccio
|
||||
|
||||
## Installation
|
||||
### A lightweight private npm proxy registry
|
||||
|
||||
|
||||
`verdaccio` is a fork of `sinopia`. It aims to keep backwards compatibility with `sinopia`, while keeping up with npm changes.
|
||||
|
||||
### Logo
|
||||
|
||||
We are looking for a new logo, ⚡⚡⚡ [please 🙏🙏 I encorage you to contribute helping us to decide](https://github.com/verdaccio/verdaccio/issues/237) ⚡⚡⚡
|
||||
|
||||
[](https://circleci.com/gh/verdaccio/verdaccio/tree/master)
|
||||
[](https://www.npmjs.org/package/verdaccio)
|
||||
[](https://www.npmjs.org/package/verdaccio)
|
||||
[](https://codecov.io/gh/verdaccio/verdaccio)
|
||||
[](https://gitter.im/verdaccio/)
|
||||
|
||||
<p align="center"><img src="https://firebasestorage.googleapis.com/v0/b/jotadeveloper-website.appspot.com/o/verdaccio_long_video2.gif?alt=media&token=4d20cad1-f700-4803-be14-4b641c651b41"></p>
|
||||
|
||||
|
||||
It allows you to have a **local npm private registry with zero configuration**. You don't have to install and replicate an entire database. Verdaccio keeps its own small database and, if a package doesn't exist there, **it asks any other registry** (npmjs.org) for it keeping only those packages you use.
|
||||
|
||||
## Introduction
|
||||
|
||||
### Use private packages
|
||||
|
||||
If you want to use all benefits of npm package system in your company without sending all code to the public, and use your private packages just as easy as public ones.
|
||||
|
||||
### Cache npmjs.org registry
|
||||
|
||||
If you have more than one server you want to install packages on, you might want to use this to decrease latency
|
||||
(presumably "slow" npmjs.org will be connected to only once per package/version) and provide limited failover (if npmjs.org is down, we might still find something useful in the cache) or avoid issues like *[How one developer just broke Node, Babel and thousands of projects in 11 lines of JavaScript](https://www.theregister.co.uk/2016/03/23/npm_left_pad_chaos/)*.
|
||||
|
||||
|
||||
### Override public packages
|
||||
|
||||
If you want to use a modified version of some 3rd-party package (for example, you found a bug, but maintainer didn't accept pull request yet), you can publish your version locally under the same name.
|
||||
|
||||
See in detail each of these [use cases](https://github.com/verdaccio/verdaccio/tree/master/wiki/use-cases.md).
|
||||
|
||||
## Get Started
|
||||
|
||||
**Help? Don't miss the [documentation section](wiki/README.md)**
|
||||
|
||||
### Prerequisites
|
||||
|
||||
* Node.js >= 4.6.1
|
||||
* `npm` or `yarn`
|
||||
|
||||
Installation and starting (application will create default config in config.yaml you can edit later)
|
||||
|
||||
```bash
|
||||
# installation and starting
|
||||
$ npm install -g sinopia
|
||||
$ sinopia
|
||||
npm install --global verdaccio
|
||||
```
|
||||
|
||||
# npm configuration
|
||||
$ npm set registry http://localhost:4387/
|
||||
Run in your terminal
|
||||
|
||||
# if you have any restricted packages (that's the point of having private
|
||||
# registry anyway), you should add this:
|
||||
$ npm set always-auth true
|
||||
```bash
|
||||
verdaccio
|
||||
```
|
||||
|
||||
# if you use HTTPS (you probably should), add an appropriate CA information
|
||||
After **npm 5.2** you can use npx which install and launch verdaccio with the same command
|
||||
|
||||
```bash
|
||||
npx verdaccio
|
||||
```
|
||||
|
||||
You would need set some npm configuration, this is optional.
|
||||
|
||||
```bash
|
||||
$ npm set registry http://localhost:4873/
|
||||
# if you use HTTPS, add an appropriate CA information
|
||||
# ("null" means get CA list from OS)
|
||||
$ npm set ca null
|
||||
```
|
||||
|
||||
## Goals
|
||||
Now you can navigate to [http://localhost:4873/](http://localhost:4873/) where your local packages will be listed and can be searched.
|
||||
|
||||
We want to create a private/caching npm repository server. The idea of it to be as simple as it could possibly be, which means "just download and run it". As I recall, there're no such things available now, is there?
|
||||
> Warning: Verdaccio current is not support PM2's cluster mode, run it with cluster mode may cause unknown behavior
|
||||
|
||||
There's two obvious use-cases:
|
||||
#### Beta
|
||||
|
||||
1. Private repository. If you want to use all benefits of npm package system in your company without sending all code to the public, you'll want that.
|
||||
If you are an adventurous developer you can use and install the latest beta version, this is a non stable version, I'd recommend only use for testing purporses.
|
||||
|
||||
2. Caching. If you have more than one server you want to install packages on, you might want to use this to decrease latency (presumably "slow" npmjs.org will be connected to only once per package/version) and provide limited failover (if npmjs.org is down, we might still find something useful in the cache).
|
||||
```bash
|
||||
$ npm install -g verdaccio@beta
|
||||
```
|
||||
|
||||
## Disclaimer
|
||||
## Publishing Private Packages
|
||||
|
||||
I don't know the internal npm stuff yet, so if npm repository heavily depends on some complex CouchDB functions, this unive^W project is doomed.
|
||||
|
||||
## Name of a project
|
||||
|
||||
Now it's "npmrepod" for "npm repository daemon". Better name suggestions are very much welcome. :)
|
||||
#### Create an user and log in
|
||||
|
||||
By the way, is it called "repository" or "registry" anyway?
|
||||
```bash
|
||||
npm adduser --registry http://localhost:4873
|
||||
```
|
||||
|
||||
## Configuration
|
||||
#### Publish your package
|
||||
|
||||
It should be able to work without any configuration, just install and run it.
|
||||
```bash
|
||||
npm publish --registry http://localhost:4873
|
||||
```
|
||||
|
||||
Of course for some advanced usage a configuration file would be necessary. So it'll probably be a javascript or yaml config. We would want to include custom functions there as plugins, so... yeah, it's probably javascript file.
|
||||
This will prompt you for user credentials which will be saved on the `verdaccio` server.
|
||||
|
||||
## Using public packages from npm.js / caching
|
||||
## Server Side Configuration
|
||||
|
||||
If some package doesn't exist in the storage, server would forward requests to npmjs.org. If npmjs.org is down, we would serve packages from cache pretending that no other packages exist. We would download only what's needed (= requested by clients), and this information would be cached forever.
|
||||
When you start a server, it auto-creates a config file. For instructions on how to run Verdaccio as a service, with a nice URL or behind a proxy have a look at the [server-side configure document](https://github.com/verdaccio/verdaccio/tree/master/wiki/server.md).
|
||||
|
||||
Example: if you successfully request express@3.0.1 from this server once, you'll able to do that again (with all it's dependencies) anytime even if npmjs.org is down. But say express@3.0.0 will not be downloaded until it's actually needed by somebody. And if npmjs.org is offline, this server would say that only express@3.0.1 (= only what's in the cache) is published, but nothing else.
|
||||
## Docker
|
||||
|
||||
Open question: can we track package changes on npmjs.org without replicating their entire database?
|
||||
[](https://hub.docker.com/r/verdaccio/verdaccio/)
|
||||
|
||||
## Features
|
||||
Below are the most commony needed informations,
|
||||
every aspect of Docker and verdaccio is [documented separately](https://github.com/verdaccio/verdaccio/tree/master/wiki/docker.md)
|
||||
|
||||
For now I'm planning to make `npm publish` and `npm install` work with this repository. Advanced features like `npm search` are so to speak not a priority.
|
||||
### Prebuilt images
|
||||
|
||||
## Access control
|
||||
To pull the latest pre-built [docker image](https://hub.docker.com/r/verdaccio/verdaccio/):
|
||||
|
||||
It is supposed to be private repository. We can't allow just anybody to see/download any package as it is in npmjs.org. So it's an open question how access control should be implemented.
|
||||
```bash
|
||||
docker pull verdaccio/verdaccio
|
||||
```
|
||||
|
||||
Maybe configuration would be simular to gitolite with working groups and such.
|
||||
Since version 2 images for every versions are availabel as [tags](https://hub.docker.com/r/verdaccio/verdaccio/tags/).
|
||||
|
||||
Should we allow anybody to publish any package by default? Should it be configurable? Shall we use users from npmjs.org or use our own user management? Well... those questions are up.
|
||||
### Running verdaccio using Docker
|
||||
|
||||
To run the docker container:
|
||||
|
||||
```bash
|
||||
docker run -it --rm --name verdaccio -p 4873:4873 verdaccio/verdaccio
|
||||
```
|
||||
|
||||
### Using docker-compose
|
||||
|
||||
1. Get the latest version of [docker-compose](https://github.com/docker/compose).
|
||||
2. Build and run the container:
|
||||
|
||||
```bash
|
||||
$ docker-compose up --build
|
||||
```
|
||||
Docker examples are available [in this repository](https://github.com/verdaccio/docker-examples).
|
||||
|
||||
* Docker + Nginx
|
||||
* Docker + Kubernetes
|
||||
* Docker + Apache
|
||||
|
||||
### Advanced Infrastructure Management Tools
|
||||
|
||||
#### Ansible
|
||||
|
||||
A Verdaccio playbook [is available at galaxy](https://galaxy.ansible.com/030/verdaccio)
|
||||
|
||||
Source: [https://github.com/verdaccio/ansible-verdaccio](https://github.com/verdaccio/ansible-verdaccio)
|
||||
|
||||
Maintainer: [@030](https://github.com/030)
|
||||
|
||||
#### Chef
|
||||
|
||||
The Verdaccio Chef cookbook [is available via the chef supermarket](https://supermarket.chef.io/cookbooks/verdaccio).
|
||||
|
||||
Source: [https://github.com/verdaccio/verdaccio-cookbook](https://github.com/verdaccio/verdaccio-cookbook).
|
||||
|
||||
Maintainer: [@kgrubb](https://github.com/kgrubb)
|
||||
|
||||
#### Puppet
|
||||
|
||||
Source: [https://github.com/verdaccio/puppet-verdaccio](https://github.com/verdaccio/puppet-verdaccio).
|
||||
|
||||
Maintainer: *No asigned yet*
|
||||
|
||||
|
||||
|
||||
## Compatibility
|
||||
|
||||
Verdaccio aims to support all features of a standard npm client that make sense to support in private repository. Unfortunately, it isn't always possible.
|
||||
|
||||
### Basic features
|
||||
|
||||
- Installing packages (npm install, npm upgrade, etc.) - **supported**
|
||||
- Publishing packages (npm publish) - **supported**
|
||||
|
||||
### Advanced package control
|
||||
|
||||
- Unpublishing packages (npm unpublish) - **supported**
|
||||
- Tagging (npm tag) - **supported**
|
||||
- Deprecation (npm deprecate) - not supported
|
||||
|
||||
### User management
|
||||
|
||||
- Registering new users (npm adduser {newuser}) - **supported**
|
||||
- Transferring ownership (npm owner add {user} {pkg}) - not supported, verdaccio uses its own acl management system
|
||||
|
||||
### Misc stuff
|
||||
|
||||
- Searching (npm search) - **supported** (cli / browser)
|
||||
- Starring (npm star, npm unstar) - not supported, doesn't make sense in private registry
|
||||
- Ping (npm ping) - **supported**
|
||||
|
||||
## Storage
|
||||
|
||||
No CouchDB. It is supposed to work with zero configuration, so filesystem would be used for storage by default.
|
||||
No CouchDB here. **This application is supposed to work with zero configuration**, so filesystem is used as a storage.
|
||||
|
||||
But our company would want to use MongoDB+GridFS for ourselves, because we have several servers with MongoDB replication set up.
|
||||
If you want to use a database instead, ask for it, we'll come up with some kind of a plugin system.
|
||||
|
||||
So, we would implement some kind of plugin system. There would be at least two plugins with the package (filesystem as a default, mongodb), but if someone wants to use CouchDB or whatever he could write a plugin himself.
|
||||
About the storage there is a running discussion [here](https://github.com/verdaccio/verdaccio/issues?q=is%3Aissue+is%3Aopen+label%3Astorage).
|
||||
|
||||
## Plugins
|
||||
|
||||
- storage (filesystem, mongo)
|
||||
- logging (bunyan interface?)
|
||||
|
||||
## Existing things
|
||||
## Similar existing things
|
||||
|
||||
- npm + git (I mean, using git+ssh:// dependencies) - most people seem to use this, but it's a terrible idea... *npm update* doesn't work, can't use git subdirectories this way, etc.
|
||||
- [reggie](https://github.com/mbrevoort/node-reggie) - this looks very interesting indeed... I might borrow some code there.
|
||||
- [shadow-npm](https://github.com/dominictarr/shadow-npm), [public service](http://shadow-npm.net/) - it uses the same code as npmjs.org + service is dead
|
||||
- [gemfury](http://www.gemfury.com/l/npm-registry) and others - those are closed-source cloud services, and I'm not in a mood to trust my private code to somebody (security through obscurity yeah!)
|
||||
- npm-registry-proxy, npm-delegate, npm-proxy - those are just proxies...
|
||||
- [nexus-repository-oss](https://www.sonatype.com/nexus-repository-oss) - Repository manager that handles more than just NPM dependencies
|
||||
- Is there something else?
|
||||
- [codebox-npm](https://github.com/craftship/codebox-npm) - Serverless private npm registry using
|
||||
- [local-npm](https://github.com/nolanlawson/local-npm) - Local and offline-first npm mirror
|
||||
|
||||
Anything else?
|
||||
## FAQ / Contact / Troubleshoot
|
||||
|
||||
If you have any issue you can try the following options, do no desist to ask or check our issues database, perhaps someone has asked already what you are looking for.
|
||||
|
||||
* [Documentation](wiki/README.md)
|
||||
* [Most common questions](https://github.com/verdaccio/verdaccio/issues?q=is%3Aissue+is%3Aopen+label%3Aquestion)
|
||||
* [Reporting a bug](https://github.com/verdaccio/verdaccio/blob/master/CONTRIBUTING.md#reporting-a-bug)
|
||||
* [Running discussions](https://github.com/verdaccio/verdaccio/issues?q=is%3Aissue+is%3Aopen+label%3Adiscuss)
|
||||
* [Chat Room](https://gitter.im/verdaccio/)
|
||||
|
||||
39
bin/sinopia
39
bin/sinopia
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var pkg_file = '../package.yaml';
|
||||
var fs = require('fs');
|
||||
var yaml = require('js-yaml');
|
||||
var commander = require('commander');
|
||||
var server = require('../lib/index');
|
||||
var crypto = require('crypto');
|
||||
var pkg = require(pkg_file);
|
||||
|
||||
commander
|
||||
.option('-l, --listen <[host:]port>', 'host:port number to listen on (default: localhost:4873)', '4873')
|
||||
.option('-c, --config <config.yaml>', 'use this configuration file (default: ./config.yaml)')
|
||||
.version(pkg.version)
|
||||
.parse(process.argv);
|
||||
|
||||
var config;
|
||||
if (commander.config) {
|
||||
config = yaml.safeLoad(fs.readFileSync(commander.config, 'utf8'));
|
||||
} else {
|
||||
try {
|
||||
config = yaml.safeLoad(fs.readFileSync('./config.yaml', 'utf8'));
|
||||
} catch(err) {
|
||||
var created_config = require('../lib/config_gen')();
|
||||
config = yaml.safeLoad(created_config.yaml);
|
||||
console.log('starting with default config, use user: "%s", pass: "%s" to authenticate', created_config.user, created_config.pass);
|
||||
fs.writeFileSync('./config.yaml', created_config.yaml);
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.user_agent) config.user_agent = 'Sinopia/'+pkg.version;
|
||||
|
||||
var hostport = commander.listen.split(':');
|
||||
if (hostport.length < 2) {
|
||||
hostport = [undefined, hostport[0]];
|
||||
}
|
||||
server(config).listen(hostport[1], hostport[0]);
|
||||
console.log('Server is listening on http://%s:%s/', hostport[0] || 'localhost', hostport[1]);
|
||||
|
||||
3
bin/verdaccio
Executable file
3
bin/verdaccio
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('../src/lib/cli');
|
||||
31
circle.yml
Normal file
31
circle.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
machine:
|
||||
environment:
|
||||
YARN_VERSION: 0.27.5
|
||||
PATH: "${PATH}:${HOME}/.yarn/bin:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
|
||||
node:
|
||||
version: 8
|
||||
dependencies:
|
||||
pre:
|
||||
- |
|
||||
if [[ ! -e ~/.yarn/bin/yarn || $(yarn --version) != "${YARN_VERSION}" ]]; then
|
||||
curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version $YARN_VERSION
|
||||
fi
|
||||
- nvm install 4
|
||||
- nvm install 6
|
||||
|
||||
cache_directories:
|
||||
- ~/.yarn
|
||||
- ~/.cache/yarn
|
||||
- "node_modules"
|
||||
override:
|
||||
- yarn install --no-progress
|
||||
test:
|
||||
override:
|
||||
- yarn run pre:ci
|
||||
- nvm alias default 6
|
||||
- yarn run test:ci
|
||||
- nvm alias default 4
|
||||
- yarn run test:ci
|
||||
- nvm alias default 8
|
||||
- yarn run test:ci
|
||||
- yarn run coverage:publish
|
||||
1
conf/README.md
Normal file
1
conf/README.md
Normal file
@@ -0,0 +1 @@
|
||||
This directory is for config examples.
|
||||
50
conf/default.yaml
Normal file
50
conf/default.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
#
|
||||
# This is the default config file. It allows all users to do anything,
|
||||
# so don't use it on production systems.
|
||||
#
|
||||
# Look here for more config file examples:
|
||||
# https://github.com/verdaccio/verdaccio/tree/master/conf
|
||||
#
|
||||
|
||||
# path to a directory with all packages
|
||||
storage: ./storage
|
||||
|
||||
auth:
|
||||
htpasswd:
|
||||
file: ./htpasswd
|
||||
# Maximum amount of users allowed to register, defaults to "+inf".
|
||||
# You can set this to -1 to disable registration.
|
||||
#max_users: 1000
|
||||
|
||||
# a list of other known repositories we can talk to
|
||||
uplinks:
|
||||
npmjs:
|
||||
url: https://registry.npmjs.org/
|
||||
|
||||
packages:
|
||||
'@*/*':
|
||||
# scoped packages
|
||||
access: $all
|
||||
publish: $authenticated
|
||||
proxy: npmjs
|
||||
|
||||
'**':
|
||||
# allow all users (including non-authenticated users) to read and
|
||||
# publish all packages
|
||||
#
|
||||
# you can specify usernames/groupnames (depending on your auth plugin)
|
||||
# and three keywords: "$all", "$anonymous", "$authenticated"
|
||||
access: $all
|
||||
|
||||
# allow all known users to publish packages
|
||||
# (anyone can register by default, remember?)
|
||||
publish: $authenticated
|
||||
|
||||
# if package is not available locally, proxy requests to 'npmjs' registry
|
||||
proxy: npmjs
|
||||
|
||||
# log settings
|
||||
logs:
|
||||
- {type: stdout, format: pretty, level: http}
|
||||
#- {type: file, path: verdaccio.log, level: info}
|
||||
|
||||
53
conf/docker.yaml
Normal file
53
conf/docker.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
#
|
||||
# This is the config file used for the docker images.
|
||||
# It allows all users to do anything, so don't use it on production systems.
|
||||
#
|
||||
# Do not configure host and port under `listen` in this file
|
||||
# as it will be ignored when using docker.
|
||||
# see https://github.com/verdaccio/verdaccio/blob/master/wiki/docker.md#docker-and-custom-port-configuration
|
||||
#
|
||||
# Look here for more config file examples:
|
||||
# https://github.com/verdaccio/verdaccio/tree/master/conf
|
||||
#
|
||||
|
||||
# path to a directory with all packages
|
||||
storage: /verdaccio/storage
|
||||
|
||||
auth:
|
||||
htpasswd:
|
||||
file: /verdaccio/conf/htpasswd
|
||||
# Maximum amount of users allowed to register, defaults to "+infinity".
|
||||
# You can set this to -1 to disable registration.
|
||||
#max_users: 1000
|
||||
|
||||
# a list of other known repositories we can talk to
|
||||
uplinks:
|
||||
npmjs:
|
||||
url: https://registry.npmjs.org/
|
||||
|
||||
packages:
|
||||
'@*/*':
|
||||
# scoped packages
|
||||
access: $all
|
||||
publish: $authenticated
|
||||
proxy: npmjs
|
||||
|
||||
'**':
|
||||
# allow all users (including non-authenticated users) to read and
|
||||
# publish all packages
|
||||
#
|
||||
# you can specify usernames/groupnames (depending on your auth plugin)
|
||||
# and three keywords: "$all", "$anonymous", "$authenticated"
|
||||
access: $all
|
||||
|
||||
# allow all known users to publish packages
|
||||
# (anyone can register by default, remember?)
|
||||
publish: $authenticated
|
||||
|
||||
# if package is not available locally, proxy requests to 'npmjs' registry
|
||||
proxy: npmjs
|
||||
|
||||
# log settings
|
||||
logs:
|
||||
- {type: stdout, format: pretty, level: http}
|
||||
#- {type: file, path: verdaccio.log, level: info}
|
||||
188
conf/full.yaml
Normal file
188
conf/full.yaml
Normal file
@@ -0,0 +1,188 @@
|
||||
# path to a directory with all packages
|
||||
storage: ./storage
|
||||
|
||||
# a list of users
|
||||
#
|
||||
# This is deprecated, use auth plugins instead (see htpasswd below).
|
||||
# users:
|
||||
# admin:
|
||||
# crypto.createHash('sha1').update(pass).digest('hex')
|
||||
# password: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3
|
||||
|
||||
web:
|
||||
# WebUI is enabled as default, if you want disable it, just uncomment this line
|
||||
#enable: false
|
||||
|
||||
title: Verdaccio
|
||||
|
||||
# logo: logo.png
|
||||
|
||||
auth:
|
||||
htpasswd:
|
||||
file: ./htpasswd
|
||||
# Maximum amount of users allowed to register, defaults to "+infinity".
|
||||
# You can set this to -1 to disable registration.
|
||||
#max_users: 1000
|
||||
|
||||
# a list of other known repositories we can talk to
|
||||
uplinks:
|
||||
npmjs:
|
||||
url: https://registry.npmjs.org/
|
||||
|
||||
# amount of time to wait for repository to respond
|
||||
# before giving up and use the local cached copy
|
||||
#timeout: 30s
|
||||
|
||||
# maximum time in which data is considered up to date
|
||||
#
|
||||
# default is 2 minutes, so server won't request the same data from
|
||||
# uplink if a similar request was made less than 2 minutes ago
|
||||
#maxage: 2m
|
||||
|
||||
# if two subsequent requests fail, no further requests will be sent to
|
||||
# this uplink for five minutes
|
||||
#max_fails: 2
|
||||
#fail_timeout: 5m
|
||||
|
||||
# timeouts are defined in the same way as nginx, see:
|
||||
# http://wiki.nginx.org/ConfigNotation
|
||||
|
||||
# add/override HTTP headers sent to the uplink server
|
||||
# this allows for HTTP Basic auth for example:
|
||||
#headers:
|
||||
# authorization: "Basic YourBase64EncodedCredentials=="
|
||||
|
||||
# set this to false to prevent tarballs from this upstream
|
||||
# to be stored in the local storage (defaults to true)
|
||||
#cache: false
|
||||
|
||||
packages:
|
||||
'@*/*':
|
||||
# scoped packages
|
||||
access: $all
|
||||
publish: $authenticated
|
||||
proxy: npmjs
|
||||
# uncomment this for packages with "local-" prefix to be available
|
||||
# for admin only, it's a recommended way of handling private packages
|
||||
#'local-*':
|
||||
# access: admin
|
||||
# publish: admin
|
||||
# # you can override storage directory for a group of packages this way:
|
||||
# storage: 'local_storage'
|
||||
|
||||
'**':
|
||||
# allow all users to read packages (including non-authenticated users)
|
||||
#
|
||||
# you can specify usernames/groupnames (depending on your auth plugin)
|
||||
# and three keywords: "$all", "$anonymous", "$authenticated"
|
||||
access: $all
|
||||
|
||||
# allow 'admin' to publish packages
|
||||
publish: $authenticated
|
||||
|
||||
# if package is not available locally, proxy requests to 'npmjs' registry
|
||||
proxy: npmjs
|
||||
|
||||
#####################################################################
|
||||
# Advanced settings
|
||||
#####################################################################
|
||||
|
||||
## Special packages publish configurations
|
||||
#publish:
|
||||
## This will allow the publisher to publish packages even if any uplink is down.
|
||||
# allow_offline: true
|
||||
|
||||
# if you use nginx with custom path, use this to override links
|
||||
#url_prefix: https://dev.company.local/verdaccio/
|
||||
|
||||
# You can specify listen address (or simply a port).
|
||||
# If you add multiple values, verdaccio will listen on all of them.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
#listen:
|
||||
# - localhost:4873 # default value
|
||||
# - http://localhost:4873 # same thing
|
||||
# - 0.0.0.0:4873 # listen on all addresses (INADDR_ANY)
|
||||
# - https://example.org:4873 # if you want to use https
|
||||
# - [::1]:4873 # ipv6
|
||||
# - unix:/tmp/verdaccio.sock # unix socket
|
||||
|
||||
# Configure HTTPS, it is required if you use "https" protocol above.
|
||||
#https:
|
||||
# key: path/to/server.key
|
||||
# cert: path/to/server.crt
|
||||
# ca: path/to/server.pem
|
||||
|
||||
# type: file | stdout | stderr
|
||||
# level: trace | debug | info | http (default) | warn | error | fatal
|
||||
#
|
||||
# parameters for file: name is filename
|
||||
# {type: 'file', path: 'verdaccio.log', level: 'debug'},
|
||||
#
|
||||
# parameters for stdout and stderr: format: json | pretty | pretty-timestamped
|
||||
# {type: 'stdout', format: 'pretty', level: 'debug'},
|
||||
logs:
|
||||
- {type: stdout, format: pretty, level: http}
|
||||
#- {type: file, path: verdaccio.log, level: info}
|
||||
|
||||
# you can specify proxy used with all requests in wget-like manner here
|
||||
# (or set up ENV variables with the same name)
|
||||
#http_proxy: http://something.local/
|
||||
#https_proxy: https://something.local/
|
||||
#no_proxy: localhost,127.0.0.1
|
||||
|
||||
# maximum size of uploaded json document
|
||||
# increase it if you have "request entity too large" errors
|
||||
#max_body_size: 1mb
|
||||
|
||||
# Notify Settings
|
||||
# Notify was built primarily to use with Slack's Incoming
|
||||
# webhooks, but will also deliver a simple payload to
|
||||
# any endpoint. Currently only active for publish / create
|
||||
# commands.
|
||||
notify:
|
||||
# Choose a method. Technically this will accept any HTTP
|
||||
# request method, but probably stick to GET or POST
|
||||
method: POST
|
||||
# Only run this notification if the package name matches the regular
|
||||
# expression
|
||||
packagePattern: ^example-package$
|
||||
# Any flags to be used with the regular expression
|
||||
packagePatternFlags: i
|
||||
# If this endpoint requires specific headers, set them here
|
||||
# as an array of key: value objects.
|
||||
headers: [{'Content-type': 'application/x-www-form-urlencoded'}]
|
||||
# set the URL endpoint for this call
|
||||
endpoint: https://hooks.slack.com/...
|
||||
# Finally, the content you will be sending in the body.
|
||||
# This data will first be run through Handlebars to parse
|
||||
# any Handlebar expressions. All data housed in the metadata object
|
||||
# is available for use within the expressions.
|
||||
content: ' {{ handlebar-expression }}'
|
||||
# For Slack, follow the following format:
|
||||
# content: '{ "text": "Package *{{ name }}* published to version *{{ dist-tags.latest }}*", "username": "Verdaccio", "icon_emoji": ":package:" }'
|
||||
|
||||
# Multiple notification endpoints can be created by specifying a collection
|
||||
'example-package-1':
|
||||
method: POST
|
||||
# Only run this notification if the package name matches the regular
|
||||
# expression
|
||||
packagePattern: ^example-package-regex$
|
||||
# Any flags to be used with the regular expression
|
||||
# since verdaccio 2.2.2 this property has been disabled read #108
|
||||
# it will be re-enabled after 2.5.0
|
||||
# packagePatternFlags: i
|
||||
# If this endpoint requires specific headers, set them here
|
||||
# as an array of key: value objects.
|
||||
# headers supports as well a literal object
|
||||
headers: {'Content-type': 'application/x-www-form-urlencoded'}
|
||||
# set the URL endpoint for this call
|
||||
endpoint: https://hooks.slack.com/...
|
||||
# Finally, the content you will be sending in the body.
|
||||
# This data will first be run through Handlebars to parse
|
||||
# any Handlebar expressions. All data housed in the metadata object
|
||||
# is available for use within the expressions.
|
||||
content: ' {{ handlebar-expression }}'
|
||||
# For Slack, follow the following format:
|
||||
# content: '{ "text": "Package *{{ name }}* published to version *{{ dist-tags.latest }}*", "username": "Verdaccio", "icon_emoji": ":package:" }'
|
||||
14
docker-compose.yaml
Normal file
14
docker-compose.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
version: '2.1'
|
||||
services:
|
||||
verdaccio:
|
||||
build: .
|
||||
container_name: verdaccio
|
||||
environment:
|
||||
- PORT
|
||||
ports:
|
||||
- $PORT:$PORT
|
||||
volumes:
|
||||
- verdaccio:/verdaccio
|
||||
volumes:
|
||||
verdaccio:
|
||||
driver: local
|
||||
9
index.js
Normal file
9
index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = require('./src/api/index');
|
||||
|
||||
/** package
|
||||
{ "name": "verdaccio",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {"js-yaml": "*"},
|
||||
"scripts": {"postinstall": "js-yaml package.yaml > package.json ; npm install"}
|
||||
}
|
||||
**/
|
||||
128
lib/config.js
128
lib/config.js
@@ -1,128 +0,0 @@
|
||||
var assert = require('assert');
|
||||
var crypto = require('crypto');
|
||||
|
||||
// [[a, [b, c]], d] -> [a, b, c, d]
|
||||
function flatten(array) {
|
||||
var result = [];
|
||||
for (var i=0; i<array.length; i++) {
|
||||
if (Array.isArray(array[i])) {
|
||||
result.push.apply(result, flatten(array[i]));
|
||||
} else {
|
||||
result.push(array[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function Config(config) {
|
||||
if (!(this instanceof Config)) return new Config(config);
|
||||
for (var i in config) {
|
||||
if (this[i] == null) this[i] = config[i];
|
||||
}
|
||||
|
||||
var users = {all:true};
|
||||
|
||||
var check_user_or_uplink = function(arg) {
|
||||
assert(arg !== 'all' || arg !== 'owner' || arg !== 'anonymous', 'CONFIG: reserved user/uplink name: ' + arg);
|
||||
assert(!arg.match(/\s/), 'CONFIG: invalid user name: ' + arg);
|
||||
assert(users[arg] == null, 'CONFIG: duplicate user/uplink name: ' + arg);
|
||||
users[arg] = true;
|
||||
};
|
||||
|
||||
['users', 'uplinks', 'packages'].forEach(function(x) {
|
||||
if (this[x] == null) this[x] = {};
|
||||
assert(
|
||||
typeof(this[x]) === 'object' &&
|
||||
!Array.isArray(this[x])
|
||||
, 'CONFIG: bad "'+x+'" value (object expected)');
|
||||
});
|
||||
|
||||
for (var i in this.users) check_user_or_uplink(i);
|
||||
for (var i in this.uplinks) check_user_or_uplink(i);
|
||||
|
||||
for (var i in this.users) {
|
||||
assert(this.users[i].password, 'CONFIG: no password for user: ' + i);
|
||||
assert(
|
||||
typeof(this.users[i].password) === 'string' &&
|
||||
this.users[i].password.match(/^[a-f0-9]{40}$/)
|
||||
, 'CONFIG: wrong password format for user: ' + i + ', sha1 expected');
|
||||
}
|
||||
|
||||
for (var i in this.uplinks) {
|
||||
assert(this.uplinks[i].url, 'CONFIG: no url for uplink: ' + i);
|
||||
assert(
|
||||
typeof(this.uplinks[i].url) === 'string'
|
||||
, 'CONFIG: wrong url format for uplink: ' + i);
|
||||
this.uplinks[i].url = this.uplinks[i].url.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
for (var i in this.packages) {
|
||||
var check_userlist = function(i, hash, action) {
|
||||
if (hash[action] == null) hash[action] = [];
|
||||
|
||||
// if it's a string, split it to array
|
||||
if (typeof(hash[action]) === 'string') {
|
||||
hash[action] = hash[action].split(/\s+/);
|
||||
}
|
||||
|
||||
assert(
|
||||
typeof(hash[action]) === 'object' &&
|
||||
Array.isArray(hash[action])
|
||||
, 'CONFIG: bad "'+i+'" package '+action+' description (array or string expected)');
|
||||
hash[action] = flatten(hash[action]);
|
||||
hash[action].forEach(function(user) {
|
||||
assert(
|
||||
users[user] != null
|
||||
, 'CONFIG: "'+i+'" package: user "'+user+'" doesn\'t exist');
|
||||
});
|
||||
}
|
||||
|
||||
assert(
|
||||
typeof(this.packages[i]) === 'object' &&
|
||||
!Array.isArray(this.packages[i])
|
||||
, 'CONFIG: bad "'+i+'" package description (object expected)');
|
||||
check_userlist(i, this.packages[i], 'access');
|
||||
check_userlist(i, this.packages[i], 'proxy');
|
||||
check_userlist(i, this.packages[i], 'publish');
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
function allow_action(package, who, action) {
|
||||
for (var i in this.packages) {
|
||||
var match_package = i == package;
|
||||
var m = i.match(/^\/(.*)\/$/);
|
||||
if (m && (new RegExp(m[1])).exec(package)) {
|
||||
match_package = true;
|
||||
}
|
||||
|
||||
if (match_package) {
|
||||
return this.packages[i][action].reduce(function(prev, curr) {
|
||||
if (curr === who || curr === 'all') return true;
|
||||
return prev;
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Config.prototype.allow_access = function(package, user) {
|
||||
return allow_action.call(this, package, user, 'access');
|
||||
}
|
||||
|
||||
Config.prototype.allow_publish = function(package, user) {
|
||||
return allow_action.call(this, package, user, 'publish');
|
||||
}
|
||||
|
||||
Config.prototype.allow_proxy = function(package, uplink) {
|
||||
return allow_action.call(this, package, uplink, 'proxy');
|
||||
}
|
||||
|
||||
Config.prototype.authenticate = function(user, password) {
|
||||
if (this.users[user] == null) return false;
|
||||
return crypto.createHash('sha1').update(password).digest('hex') === this.users[user].password;
|
||||
}
|
||||
|
||||
module.exports = Config;
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# path to a directory with all packages
|
||||
storage: ./storage
|
||||
|
||||
users:
|
||||
admin:
|
||||
# crypto.createHash('sha1').update(pass).digest('hex')
|
||||
password: __PASSWORD__
|
||||
|
||||
uplinks:
|
||||
npmjs:
|
||||
url: https://registry.npmjs.org/
|
||||
|
||||
packages:
|
||||
'/.*/':
|
||||
publish: admin
|
||||
access: all
|
||||
proxy: npmjs
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
var fs = require('fs');
|
||||
var crypto = require('crypto');
|
||||
|
||||
module.exports = function create_config() {
|
||||
var pass = crypto.randomBytes(8).toString('base64').replace(/[=+\/]/g, '');
|
||||
var pass_digest = crypto.createHash('sha1').update(pass).digest('hex');
|
||||
var config = fs.readFileSync(require.resolve('./config_def.yaml'), 'utf8');
|
||||
config = config.replace('__PASSWORD__', pass_digest);
|
||||
|
||||
return {
|
||||
yaml: config,
|
||||
user: 'admin',
|
||||
pass: pass,
|
||||
};
|
||||
}
|
||||
|
||||
51
lib/error.js
51
lib/error.js
@@ -1,51 +0,0 @@
|
||||
var util = require('util');
|
||||
|
||||
function parse_error_params(params, status, msg) {
|
||||
if (typeof(params) === 'string') {
|
||||
return {
|
||||
msg: params,
|
||||
status: status,
|
||||
};
|
||||
} else if (typeof(params) === 'number') {
|
||||
return {
|
||||
msg: msg,
|
||||
status: params,
|
||||
};
|
||||
} else if (typeof(params) === 'object' && params != null) {
|
||||
if (params.msg == null) params.msg = msg;
|
||||
if (params.status == null) params.status = status;
|
||||
return params;
|
||||
} else {
|
||||
return {
|
||||
msg: msg,
|
||||
status: status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Errors caused by malfunctioned code
|
||||
*/
|
||||
var AppError = function(params, constr) {
|
||||
Error.captureStackTrace(this, constr || this);
|
||||
params = parse_error_params(params, 500, 'Internal server error');
|
||||
this.msg = params.msg;
|
||||
this.status = params.status;
|
||||
};
|
||||
util.inherits(AppError, Error);
|
||||
AppError.prototype.name = 'Application Error';
|
||||
|
||||
/*
|
||||
* Errors caused by wrong request
|
||||
*/
|
||||
var UserError = function(params, constr) {
|
||||
params = parse_error_params(params, 404, 'The requested resource was not found');
|
||||
this.msg = params.msg;
|
||||
this.status = params.status;
|
||||
};
|
||||
util.inherits(UserError, Error);
|
||||
UserError.prototype.name = 'User Error';
|
||||
|
||||
module.exports.AppError = AppError;
|
||||
module.exports.UserError = UserError;
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
var fs = require('fs');
|
||||
var Path = require('path');
|
||||
|
||||
function make_directories(dest, cb) {
|
||||
var dir = Path.dirname(dest);
|
||||
if (dir === '.' || dir === '..') return cb();
|
||||
fs.mkdir(dir, function(err) {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
make_directories(dir, function() {
|
||||
fs.mkdir(dir, cb);
|
||||
})
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function write_file(dest, data, cb) {
|
||||
var safe_write = function(cb) {
|
||||
fs.writeFile(dest, data, cb);
|
||||
}
|
||||
|
||||
safe_write(function(err) {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
make_directories(dest, function() {
|
||||
safe_write(cb);
|
||||
})
|
||||
} else {
|
||||
cb(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function create(name, contents, callback) {
|
||||
fs.exists(name, function(exists) {
|
||||
if (exists) return callback(new Error({code: 'EEXISTS'}));
|
||||
write_file(name, contents, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function update(name, contents, callback) {
|
||||
fs.exists(name, function(exists) {
|
||||
if (!exists) return callback(new Error({code: 'ENOENT'}));
|
||||
write_file(name, contents, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function read(name, callback) {
|
||||
fs.readFile(name, callback);
|
||||
}
|
||||
|
||||
function Storage(path) {
|
||||
this.path = path;
|
||||
try {
|
||||
fs.mkdirSync(path);
|
||||
console.log('created new packages directory: ', path);
|
||||
} catch(err) {
|
||||
if (err.code !== 'EEXIST') throw new Error(err);
|
||||
}
|
||||
}
|
||||
|
||||
Storage.prototype.read = function(name, cb) {
|
||||
read(this.path + '/' + name, cb);
|
||||
}
|
||||
|
||||
Storage.prototype.read_json = function(name, cb) {
|
||||
read(this.path + '/' + name, function(err, res) {
|
||||
if (err) return cb(err);
|
||||
cb(null, JSON.parse(res));
|
||||
});
|
||||
}
|
||||
|
||||
Storage.prototype.create = function(name, value, cb) {
|
||||
create(this.path + '/' + name, value, cb);
|
||||
}
|
||||
|
||||
Storage.prototype.create_json = function(name, value, cb) {
|
||||
create(this.path + '/' + name, JSON.stringify(value), cb);
|
||||
}
|
||||
|
||||
Storage.prototype.update = function(name, value, cb) {
|
||||
update(this.path + '/' + name, value, cb);
|
||||
}
|
||||
|
||||
Storage.prototype.update_json = function(name, value, cb) {
|
||||
update(this.path + '/' + name, JSON.stringify(value), cb);
|
||||
}
|
||||
|
||||
module.exports = Storage;
|
||||
|
||||
198
lib/index.js
198
lib/index.js
@@ -1,198 +0,0 @@
|
||||
var express = require('express');
|
||||
var cookies = require('cookies');
|
||||
var utils = require('./utils');
|
||||
var Storage = require('./storage');
|
||||
var Config = require('./config');
|
||||
var UError = require('./error').UserError;
|
||||
var basic_auth = require('./middleware').basic_auth;
|
||||
var validate_name = require('./middleware').validate_name;
|
||||
var media = require('./middleware').media;
|
||||
var expect_json = require('./middleware').expect_json;
|
||||
|
||||
module.exports = function(config_hash) {
|
||||
var config = new Config(config_hash);
|
||||
var storage = new Storage(config);
|
||||
|
||||
var can = function(action) {
|
||||
return function(req, res, next) {
|
||||
if (config['allow_'+action](req.params.package, req.remoteUser)) {
|
||||
next();
|
||||
} else {
|
||||
next(new UError({
|
||||
status: 403,
|
||||
msg: 'user '+req.remoteUser+' not allowed to '+action+' it'
|
||||
}));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var app = express();
|
||||
app.use(express.logger());
|
||||
app.use(basic_auth(function(user, pass) {
|
||||
return config.authenticate(user, pass);
|
||||
}));
|
||||
app.use(express.bodyParser());
|
||||
|
||||
app.param('package', validate_name);
|
||||
app.param('filename', validate_name);
|
||||
|
||||
/* app.get('/', function(req, res) {
|
||||
res.send({
|
||||
error: 'unimplemented'
|
||||
});
|
||||
});*/
|
||||
|
||||
/* app.get('/-/all', function(req, res) {
|
||||
var https = require('https');
|
||||
var JSONStream = require('JSONStream');
|
||||
var request = require('request')({
|
||||
url: 'https://registry.npmjs.org/-/all',
|
||||
ca: require('./npmsslkeys'),
|
||||
})
|
||||
.pipe(JSONStream.parse('*'))
|
||||
.on('data', function(d) {
|
||||
console.log(d);
|
||||
});
|
||||
});*/
|
||||
|
||||
// TODO: anonymous user?
|
||||
app.get('/:package/:version?', can('access'), function(req, res, next) {
|
||||
storage.get_package(req.params.package, function(err, info) {
|
||||
if (err) return next(err);
|
||||
|
||||
// XXX: in some cases npm calls for /:package and for some cases
|
||||
// for /:package/:version - should investigate that
|
||||
if (req.params.version) {
|
||||
if (info.versions[req.params.version] != null) {
|
||||
info = info.versions[req.params.version];
|
||||
} else {
|
||||
return next(new UError({
|
||||
status: 404,
|
||||
msg: 'version not found: ' + req.params.version
|
||||
}));
|
||||
}
|
||||
}
|
||||
res.send(info);
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/:package/-/:filename', can('access'), function(req, res, next) {
|
||||
storage.get_tarball(req.params.package, req.params.filename, function(err, stream) {
|
||||
if (err) return next(err);
|
||||
if (!stream) {
|
||||
return next(new UError({
|
||||
status: 404,
|
||||
msg: 'package not found'
|
||||
}));
|
||||
}
|
||||
res.header('content-type', 'application/octet-stream');
|
||||
res.send(stream);
|
||||
});
|
||||
});
|
||||
|
||||
//app.get('/*', function(req, res) {
|
||||
// proxy.request(req, res);
|
||||
//});
|
||||
|
||||
// placeholder 'cause npm require to be authenticated to publish
|
||||
// we do not do any real authentication yet
|
||||
app.post('/_session', cookies.express(), function(req, res) {
|
||||
res.cookies.set('AuthSession', String(Math.random()), {
|
||||
// npmjs.org sets 10h expire
|
||||
expires: new Date(Date.now() + 10*60*60*1000)
|
||||
});
|
||||
res.send({"ok":true,"name":"somebody","roles":[]});
|
||||
});
|
||||
|
||||
app.get('/-/user/:argument', function(req, res, next) {
|
||||
// can't put 'org.couchdb.user' in route address for some reason
|
||||
if (req.params.argument.split(':')[0] !== 'org.couchdb.user') return next('route');
|
||||
res.status(200);
|
||||
return res.send({
|
||||
ok: 'you are authenticated as "' + req.user + '"',
|
||||
});
|
||||
});
|
||||
|
||||
app.put('/-/user/:argument', function(req, res, next) {
|
||||
// can't put 'org.couchdb.user' in route address for some reason
|
||||
if (req.params.argument.split(':')[0] !== 'org.couchdb.user') return next('route');
|
||||
res.status(409);
|
||||
return res.send({
|
||||
error: 'registration is not implemented',
|
||||
});
|
||||
});
|
||||
|
||||
app.put('/-/user/:argument/-rev/*', function(req, res, next) {
|
||||
// can't put 'org.couchdb.user' in route address for some reason
|
||||
if (req.params.argument.split(':')[0] !== 'org.couchdb.user') return next('route');
|
||||
res.status(201);
|
||||
return res.send({
|
||||
ok: 'you are authenticated as "' + req.user + '"',
|
||||
});
|
||||
});
|
||||
|
||||
// publishing a package
|
||||
app.put('/:package', can('publish'), media('application/json'), expect_json, function(req, res, next) {
|
||||
var name = req.params.package;
|
||||
try {
|
||||
var metadata = utils.validate_metadata(req.body, name);
|
||||
} catch(err) {
|
||||
return next(new UError({
|
||||
status: 422,
|
||||
msg: 'bad incoming package data',
|
||||
}));
|
||||
}
|
||||
|
||||
storage.add_package(name, metadata, function(err) {
|
||||
if (err) return next(err);
|
||||
res.status(201);
|
||||
return res.send({
|
||||
ok: 'created new package'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// uploading package tarball
|
||||
app.put('/:package/-/:filename/*', can('publish'), media('application/octet-stream'), function(req, res, next) {
|
||||
var name = req.params.package;
|
||||
|
||||
storage.add_tarball(name, req.params.filename, req, function(err) {
|
||||
if (err) return next(err);
|
||||
res.status(201);
|
||||
return res.send({
|
||||
ok: 'tarball uploaded successfully'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// adding a version
|
||||
app.put('/:package/:version/-tag/:tag', can('publish'), media('application/json'), expect_json, function(req, res, next) {
|
||||
var name = req.params.package;
|
||||
var version = req.params.version;
|
||||
var tag = req.params.tag;
|
||||
|
||||
storage.add_version(name, version, req.body, tag, function(err) {
|
||||
if (err) return next(err);
|
||||
res.status(201);
|
||||
return res.send({
|
||||
ok: 'package published'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.use(app.router);
|
||||
app.use(function(err, req, res, next) {
|
||||
if (err.status && err.msg && err.status >= 400 && err.status < 600) {
|
||||
res.status(err.status);
|
||||
res.send({error: err.msg});
|
||||
} else {
|
||||
console.log(err);
|
||||
console.log(err.stack);
|
||||
res.status(500);
|
||||
res.send({error: 'internal server error'});
|
||||
}
|
||||
});
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
var utils = require('./utils');
|
||||
var UError = require('./error').UserError;
|
||||
|
||||
module.exports.validate_name = function validate_name(req, res, next, value, name) {
|
||||
if (utils.validate_name(req.params.package)) {
|
||||
req.params.package = String(req.params.package);
|
||||
next();
|
||||
} else {
|
||||
next(new UError({
|
||||
status: 403,
|
||||
msg: 'invalid package name',
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.media = function media(expect) {
|
||||
return function(req, res, next) {
|
||||
if (req.headers['content-type'] !== expect) {
|
||||
next(new UError({
|
||||
status: 415,
|
||||
msg: 'wrong content-type, we expect '+expect,
|
||||
}));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.expect_json = function expect_json(req, res, next) {
|
||||
if (typeof(req.body) !== 'object') {
|
||||
return next({
|
||||
status: 400,
|
||||
msg: 'can\'t parse incoming json',
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports.basic_auth = function basic_auth(callback) {
|
||||
return function(req, res, next) {
|
||||
var authorization = req.headers.authorization;
|
||||
|
||||
if (req.user) return next();
|
||||
if (authorization == null) {
|
||||
req.user = req.remoteUser = 'anonymous';
|
||||
return next();
|
||||
}
|
||||
|
||||
var parts = authorization.split(' ');
|
||||
|
||||
if (parts.length !== 2) return next({
|
||||
status: 400,
|
||||
msg: 'bad authorization header',
|
||||
});
|
||||
|
||||
var scheme = parts[0]
|
||||
, credentials = new Buffer(parts[1], 'base64').toString()
|
||||
, index = credentials.indexOf(':');
|
||||
|
||||
if ('Basic' != scheme || index < 0) return next({
|
||||
status: 400,
|
||||
msg: 'bad authorization header',
|
||||
});
|
||||
|
||||
var user = credentials.slice(0, index)
|
||||
, pass = credentials.slice(index + 1);
|
||||
|
||||
if (callback(user, pass)) {
|
||||
req.user = req.remoteUser = user;
|
||||
next();
|
||||
} else {
|
||||
next({
|
||||
status: 403,
|
||||
msg: 'bad username/password, access denied',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
//
|
||||
// Get this thingy from `npmconf` package if it ever changes...
|
||||
//
|
||||
|
||||
module.exports = // the npm CA certificate.
|
||||
[ "-----BEGIN CERTIFICATE-----\n"+
|
||||
"MIIChzCCAfACCQDauvz/KHp8ejANBgkqhkiG9w0BAQUFADCBhzELMAkGA1UEBhMC\n"+
|
||||
"VVMxCzAJBgNVBAgTAkNBMRAwDgYDVQQHEwdPYWtsYW5kMQwwCgYDVQQKEwNucG0x\n"+
|
||||
"IjAgBgNVBAsTGW5wbSBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxDjAMBgNVBAMTBW5w\n"+
|
||||
"bUNBMRcwFQYJKoZIhvcNAQkBFghpQGl6cy5tZTAeFw0xMTA5MDUwMTQ3MTdaFw0y\n"+
|
||||
"MTA5MDIwMTQ3MTdaMIGHMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEDAOBgNV\n"+
|
||||
"BAcTB09ha2xhbmQxDDAKBgNVBAoTA25wbTEiMCAGA1UECxMZbnBtIENlcnRpZmlj\n"+
|
||||
"YXRlIEF1dGhvcml0eTEOMAwGA1UEAxMFbnBtQ0ExFzAVBgkqhkiG9w0BCQEWCGlA\n"+
|
||||
"aXpzLm1lMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLI4tIqPpRW+ACw9GE\n"+
|
||||
"OgBlJZwK5f8nnKCLK629Pv5yJpQKs3DENExAyOgDcyaF0HD0zk8zTp+ZsLaNdKOz\n"+
|
||||
"Gn2U181KGprGKAXP6DU6ByOJDWmTlY6+Ad1laYT0m64fERSpHw/hjD3D+iX4aMOl\n"+
|
||||
"y0HdbT5m1ZGh6SJz3ZqxavhHLQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAC4ySDbC\n"+
|
||||
"l7W1WpLmtLGEQ/yuMLUf6Jy/vr+CRp4h+UzL+IQpCv8FfxsYE7dhf/bmWTEupBkv\n"+
|
||||
"yNL18lipt2jSvR3v6oAHAReotvdjqhxddpe5Holns6EQd1/xEZ7sB1YhQKJtvUrl\n"+
|
||||
"ZNufy1Jf1r0ldEGeA+0ISck7s+xSh9rQD2Op\n"+
|
||||
"-----END CERTIFICATE-----\n",
|
||||
|
||||
// "GlobalSign Root CA"
|
||||
"-----BEGIN CERTIFICATE-----\n"+
|
||||
"MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx\n"+
|
||||
"GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds\n"+
|
||||
"b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV\n"+
|
||||
"BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD\n"+
|
||||
"VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa\n"+
|
||||
"DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc\n"+
|
||||
"THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb\n"+
|
||||
"Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP\n"+
|
||||
"c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX\n"+
|
||||
"gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n"+
|
||||
"HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF\n"+
|
||||
"AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj\n"+
|
||||
"Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG\n"+
|
||||
"j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH\n"+
|
||||
"hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC\n"+
|
||||
"X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==\n"+
|
||||
"-----END CERTIFICATE-----\n",
|
||||
|
||||
// "GlobalSign Root CA - R2"
|
||||
"-----BEGIN CERTIFICATE-----\n"+
|
||||
"MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xv\n"+
|
||||
"YmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh\n"+
|
||||
"bFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT\n"+
|
||||
"aWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln\n"+
|
||||
"bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6\n"+
|
||||
"ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozp\n"+
|
||||
"s6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjN\n"+
|
||||
"S7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CL\n"+
|
||||
"TfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6C\n"+
|
||||
"ygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E\n"+
|
||||
"FgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9i\n"+
|
||||
"YWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjAN\n"+
|
||||
"BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp\n"+
|
||||
"9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu\n"+
|
||||
"01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG7\n"+
|
||||
"9G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7\n"+
|
||||
"TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==\n"+
|
||||
"-----END CERTIFICATE-----\n" ]
|
||||
24
lib/proxy.js
24
lib/proxy.js
@@ -1,24 +0,0 @@
|
||||
var https = require('https');
|
||||
|
||||
module.exports.request = function(req, resp) {
|
||||
https.get({
|
||||
host: 'registry.npmjs.org',
|
||||
path: req.url,
|
||||
ca: require('./npmsslkeys'),
|
||||
headers: {
|
||||
'User-Agent': 'sinopia/0.0.0',
|
||||
},
|
||||
}, function(res) {
|
||||
resp.writeHead(res.statusCode, res.headers);
|
||||
res.on('data', function(d) {
|
||||
resp.write(d);
|
||||
});
|
||||
res.on('end', function() {
|
||||
resp.end();
|
||||
});
|
||||
}).on('error', function(err) {
|
||||
console.error(err);
|
||||
resp.send(500);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
var fs_storage = require('./fs-storage');
|
||||
var UError = require('./error').UserError;
|
||||
var info_file = 'package.json';
|
||||
var fs = require('fs');
|
||||
|
||||
function Storage(config) {
|
||||
if (!(this instanceof Storage)) return new Storage(config);
|
||||
this.config = config;
|
||||
this.storage = new fs_storage(this.config.storage);
|
||||
return this;
|
||||
}
|
||||
|
||||
Storage.prototype.add_package = function(name, metadata, callback) {
|
||||
this.storage.create_json(name + '/' + info_file, metadata, function(err) {
|
||||
if (err && err.code === 'EEXISTS') {
|
||||
return callback(new UError({
|
||||
status: 409,
|
||||
msg: 'this package is already present'
|
||||
}));
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
Storage.prototype.add_version = function(name, version, metadata, tag, callback) {
|
||||
var self = this;
|
||||
self.storage.read_json(name + '/' + info_file, function(err, data) {
|
||||
// TODO: race condition
|
||||
if (err) return callback(err);
|
||||
|
||||
if (data.versions[version] != null) {
|
||||
return callback(new UError({
|
||||
status: 409,
|
||||
msg: 'this version already present'
|
||||
}));
|
||||
}
|
||||
data.versions[version] = metadata;
|
||||
data['dist-tags'][tag] = version;
|
||||
self.storage.update_json(name + '/' + info_file, data, callback);
|
||||
});
|
||||
}
|
||||
|
||||
Storage.prototype.add_tarball = function(name, filename, stream, callback) {
|
||||
var self = this;
|
||||
if (name === info_file) {
|
||||
return callback(new UError({
|
||||
status: 403,
|
||||
msg: 'can\'t use this filename'
|
||||
}));
|
||||
}
|
||||
|
||||
var data = new Buffer(0);
|
||||
stream.on('data', function(d) {
|
||||
var tmp = data;
|
||||
data = new Buffer(tmp.length+d.length);
|
||||
tmp.copy(data, 0);
|
||||
d.copy(data, tmp.length);
|
||||
});
|
||||
stream.on('end', function(d) {
|
||||
self.storage.create(name + '/' + filename, data, function(err) {
|
||||
if (err && err.code === 'EEXISTS') {
|
||||
return callback(new UError({
|
||||
status: 409,
|
||||
msg: 'this tarball is already present'
|
||||
}));
|
||||
}
|
||||
callback.apply(null, arguments);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Storage.prototype.get_tarball = function(name, filename, callback) {
|
||||
this.storage.read(name + '/' + filename, function(err) {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
return callback(new UError({
|
||||
status: 404,
|
||||
msg: 'no such package available'
|
||||
}));
|
||||
}
|
||||
callback.apply(null, arguments);
|
||||
});
|
||||
}
|
||||
|
||||
Storage.prototype.get_package = function(name, callback) {
|
||||
this.storage.read_json(name + '/' + info_file, function(err) {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
return callback(new UError({
|
||||
status: 404,
|
||||
msg: 'no such package available'
|
||||
}));
|
||||
}
|
||||
callback.apply(null, arguments);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Storage;
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
var request = require('request');
|
||||
var UError = require('./error').UserError;
|
||||
var URL = require('url');
|
||||
|
||||
function Storage(name, config) {
|
||||
if (!(this instanceof Storage)) return new Storage(config);
|
||||
this.config = config;
|
||||
this.name = name;
|
||||
this.ca;
|
||||
|
||||
if (URL.parse(this.config.uplinks[this.name].url).hostname === 'registry.npmjs.org') {
|
||||
this.ca = require('./npmsslkeys');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
Storage.prototype.get_package = function(name, callback) {
|
||||
request({
|
||||
url: this.config.uplinks[this.name].url + '/' + name,
|
||||
json: true,
|
||||
headers: {
|
||||
'User-Agent': this.config.user_agent,
|
||||
},
|
||||
ca: this.ca,
|
||||
}, function(err, res, body) {
|
||||
if (err) return callback(err);
|
||||
if (res.statusCode === 404) {
|
||||
return callback(new UError({
|
||||
msg: 'package doesn\'t exist on uplink',
|
||||
status: 404,
|
||||
}));
|
||||
}
|
||||
if (!(res.statusCode >= 200 && res.statusCode < 300)) {
|
||||
return callback(new Error('bad status code: ' + res.statusCode));
|
||||
}
|
||||
callback(null, body);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Storage;
|
||||
|
||||
131
lib/storage.js
131
lib/storage.js
@@ -1,131 +0,0 @@
|
||||
var async = require('async');
|
||||
var semver = require('semver');
|
||||
var UError = require('./error').UserError;
|
||||
var Local = require('./st-local');
|
||||
var Proxy = require('./st-proxy');
|
||||
var utils = require('./utils');
|
||||
|
||||
function Storage(config) {
|
||||
if (!(this instanceof Storage)) return new Storage(config);
|
||||
|
||||
this.config = config;
|
||||
this.uplinks = {};
|
||||
for (var p in config.uplinks) {
|
||||
this.uplinks[p] = new Proxy(p, config);
|
||||
}
|
||||
this.local = new Local(config);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
Storage.prototype.add_package = function(name, metadata, callback) {
|
||||
var self = this;
|
||||
|
||||
var uplinks = [];
|
||||
for (var i in this.uplinks) {
|
||||
if (this.config.allow_proxy(name, i)) {
|
||||
uplinks.push(this.uplinks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
async.map(uplinks, function(up, cb) {
|
||||
up.get_package(name, function(err, res) {
|
||||
cb(null, [err, res]);
|
||||
});
|
||||
}, function(err, results) {
|
||||
for (var i=0; i<results.length; i++) {
|
||||
// checking error
|
||||
// if uplink fails with a status other than 404, we report failure
|
||||
if (results[i][0] != null) {
|
||||
if (results[i][0].status !== 404) {
|
||||
return callback(new UError({
|
||||
status: 503,
|
||||
msg: 'one of the uplinks is down, refuse to publish'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// checking package
|
||||
if (results[i][1] != null) {
|
||||
return callback(new UError({
|
||||
status: 409,
|
||||
msg: 'this package is already present'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
self.local.add_package(name, metadata, callback);
|
||||
});
|
||||
}
|
||||
|
||||
Storage.prototype.add_version = function(name, version, metadata, tag, callback) {
|
||||
this.local.add_version(name, version, metadata, tag, callback);
|
||||
}
|
||||
|
||||
Storage.prototype.add_tarball = function(name, filename, stream, callback) {
|
||||
this.local.add_tarball(name, filename, stream, callback);
|
||||
}
|
||||
|
||||
Storage.prototype.get_tarball = function(name, filename, callback) {
|
||||
this.local.get_tarball(name, filename, callback);
|
||||
}
|
||||
|
||||
Storage.prototype.get_package = function(name, callback) {
|
||||
var uplinks = [this.local];
|
||||
for (var i in this.uplinks) {
|
||||
if (this.config.allow_proxy(name, i)) {
|
||||
uplinks.push(this.uplinks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var result = {
|
||||
name: name,
|
||||
versions: {},
|
||||
'dist-tags': {},
|
||||
};
|
||||
var exists = false;
|
||||
var latest;
|
||||
|
||||
async.map(uplinks, function(up, cb) {
|
||||
up.get_package(name, function(err, up_res) {
|
||||
if (err) return cb();
|
||||
|
||||
try {
|
||||
utils.validate_metadata(up_res, name);
|
||||
} catch(err) {
|
||||
return cb();
|
||||
}
|
||||
|
||||
var this_version = up_res['dist-tags'].latest;
|
||||
if (!semver.gt(latest, this_version) && this_version) {
|
||||
latest = this_version;
|
||||
var is_latest = true;
|
||||
}
|
||||
|
||||
['versions', 'dist-tags'].forEach(function(key) {
|
||||
for (var i in up_res[key]) {
|
||||
if (!result[key][i] || is_latest) {
|
||||
result[key][i] = up_res[key][i];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// if we got to this point, assume that the correct package exists
|
||||
// on the uplink
|
||||
exists = true;
|
||||
cb();
|
||||
});
|
||||
}, function(err) {
|
||||
if (err) return callback(err);
|
||||
if (!exists) {
|
||||
return callback(new UError({
|
||||
status: 404,
|
||||
msg: 'no such package available'
|
||||
}));
|
||||
}
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Storage;
|
||||
|
||||
37
lib/utils.js
37
lib/utils.js
@@ -1,37 +0,0 @@
|
||||
var assert = require('assert');
|
||||
|
||||
// from normalize-package-data/lib/fixer.js
|
||||
module.exports.validate_name = function(name) {
|
||||
if (
|
||||
name.charAt(0) === "." ||
|
||||
name.match(/[\/@\s\+%:]/) ||
|
||||
name !== encodeURIComponent(name) ||
|
||||
name.toLowerCase() === "node_modules" ||
|
||||
name.toLowerCase() === "favicon.ico"
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function is_object(obj) {
|
||||
return typeof(obj) === 'object' && !Array.isArray(obj);
|
||||
}
|
||||
|
||||
module.exports.validate_metadata = function(object, name) {
|
||||
assert(is_object(object));
|
||||
assert.equal(object._id, name);
|
||||
assert.equal(object.name, name);
|
||||
|
||||
if (!is_object(object['dist-tags'])) {
|
||||
object['dist-tags'] = {};
|
||||
}
|
||||
|
||||
if (!is_object(object['versions'])) {
|
||||
object['versions'] = {};
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
153
package.json
Normal file
153
package.json
Normal file
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"name": "verdaccio",
|
||||
"version": "2.4.0",
|
||||
"description": "Private npm repository server",
|
||||
"author": {
|
||||
"name": "Alex Kocharin",
|
||||
"email": "alex@kocharin.ru"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/verdaccio/verdaccio"
|
||||
},
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
"verdaccio": "./bin/verdaccio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@verdaccio/file-locking": "^0.0.3",
|
||||
"@verdaccio/streams": "^0.0.2",
|
||||
"JSONStream": "^1.1.1",
|
||||
"apache-md5": "^1.1.2",
|
||||
"async": "^2.0.1",
|
||||
"body-parser": "^1.15.0",
|
||||
"bunyan": "^1.8.0",
|
||||
"chalk": "^2.0.1",
|
||||
"commander": "^2.11.0",
|
||||
"compression": "1.6.2",
|
||||
"cookies": "^0.7.0",
|
||||
"cors": "^2.8.3",
|
||||
"express": "4.15.3",
|
||||
"global": "^4.3.2",
|
||||
"handlebars": "4.0.5",
|
||||
"http-errors": "^1.4.0",
|
||||
"js-string-escape": "1.0.1",
|
||||
"js-yaml": "^3.6.0",
|
||||
"jsonwebtoken": "^7.4.1",
|
||||
"lockfile": "^1.0.1",
|
||||
"lodash": "4.17.4",
|
||||
"lunr": "^0.7.0",
|
||||
"marked": "0.3.6",
|
||||
"mime": "^1.3.6",
|
||||
"minimatch": "^3.0.2",
|
||||
"mkdirp": "^0.5.1",
|
||||
"pkginfo": "^0.4.0",
|
||||
"request": "^2.72.0",
|
||||
"semver": "^5.1.0",
|
||||
"unix-crypt-td-js": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "0.16.2",
|
||||
"babel-cli": "6.24.1",
|
||||
"babel-core": "6.25.0",
|
||||
"babel-eslint": "7.2.3",
|
||||
"babel-loader": "7.1.1",
|
||||
"babel-plugin-flow-runtime": "0.11.1",
|
||||
"babel-plugin-transform-decorators-legacy": "1.3.4",
|
||||
"babel-plugin-transform-runtime": "6.23.0",
|
||||
"babel-preset-env": "1.5.2",
|
||||
"babel-preset-flow": "6.23.0",
|
||||
"babel-preset-react": "6.24.1",
|
||||
"babel-preset-stage-2": "6.24.1",
|
||||
"babel-preset-stage-3": "6.24.1",
|
||||
"babel-runtime": "6.23.0",
|
||||
"codacy-coverage": "2.0.2",
|
||||
"codecov": "2.2.0",
|
||||
"coveralls": "2.13.1",
|
||||
"css-loader": "0.28.4",
|
||||
"element-react": "1.0.16",
|
||||
"element-theme-default": "1.3.7",
|
||||
"eslint": "4.2.0",
|
||||
"eslint-config-google": "0.8.0",
|
||||
"eslint-loader": "1.8.0",
|
||||
"eslint-plugin-babel": "4.1.1",
|
||||
"eslint-plugin-flowtype": "2.35.0",
|
||||
"eslint-plugin-import": "2.6.1",
|
||||
"eslint-plugin-react": "7.1.0",
|
||||
"extract-text-webpack-plugin": "3.0.0",
|
||||
"file-loader": "0.11.2",
|
||||
"flow-runtime": "0.13.0",
|
||||
"friendly-errors-webpack-plugin": "1.6.1",
|
||||
"fs-extra": "4.0.1",
|
||||
"github-markdown-css": "2.8.0",
|
||||
"html-webpack-plugin": "2.29.0",
|
||||
"in-publish": "2.0.0",
|
||||
"localstorage-memory": "1.0.2",
|
||||
"mocha": "3.4.2",
|
||||
"mocha-lcov-reporter": "1.3.0",
|
||||
"node-sass": "4.5.3",
|
||||
"normalize.css": "7.0.0",
|
||||
"nyc": "11.0.3",
|
||||
"ora": "1.3.0",
|
||||
"prop-types": "15.5.10",
|
||||
"react": "15.6.1",
|
||||
"react-dom": "15.6.1",
|
||||
"react-hot-loader": "3.0.0-beta.7",
|
||||
"react-router-dom": "4.1.1",
|
||||
"react-syntax-highlighter": "5.6.2",
|
||||
"rimraf": "2.6.1",
|
||||
"sass-loader": "6.0.6",
|
||||
"source-map-loader": "0.2.1",
|
||||
"standard-version": "4.2.0",
|
||||
"style-loader": "0.18.2",
|
||||
"stylelint": "7.13.0",
|
||||
"stylelint-config-standard": "16.0.0",
|
||||
"stylelint-webpack-plugin": "0.8.0",
|
||||
"url-loader": "0.5.8",
|
||||
"webpack": "3.2.0",
|
||||
"webpack-dev-server": "2.5.0",
|
||||
"webpack-merge": "4.1.0"
|
||||
},
|
||||
"keywords": [
|
||||
"private",
|
||||
"package",
|
||||
"repository",
|
||||
"registry",
|
||||
"enterprise",
|
||||
"modules",
|
||||
"proxy",
|
||||
"server"
|
||||
],
|
||||
"scripts": {
|
||||
"release": "standard-version -a -s",
|
||||
"prepublish": "in-publish && npm run build:webui || not-in-publish",
|
||||
"test": "mocha ./test/functional ./test/unit --reporter=spec --full-trace",
|
||||
"pre:ci": "npm run build:webui",
|
||||
"test:ci": "npm run test:coverage",
|
||||
"test:only": "mocha ./test/functional ./test/unit",
|
||||
"test:coverage": "nyc npm t",
|
||||
"coverage:html": "nyc report --reporter=html",
|
||||
"coverage:publish": "nyc report --reporter=lcov | codecov",
|
||||
"lint": "eslint .",
|
||||
"lint:css": "stylelint 'src/**/*.scss' --syntax scss",
|
||||
"pre:webpack": "npm run lint && rimraf static/*",
|
||||
"dev:webui": "babel-node tools/dev.server.js",
|
||||
"build:webui": "npm run pre:webpack && webpack --config tools/webpack.prod.config.babel.js",
|
||||
"build:docker": "docker build -t verdaccio . --no-cache",
|
||||
"build:docker:rpi": "docker build -f Dockerfile.rpi -t verdaccio:rpi ."
|
||||
},
|
||||
"jest": {
|
||||
"snapshotSerializers": [
|
||||
"jest-serializer-enzyme"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.6.1",
|
||||
"npm": ">=2.15.9"
|
||||
},
|
||||
"preferGlobal": true,
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"license": "WTFPL"
|
||||
}
|
||||
26
package.yaml
26
package.yaml
@@ -1,26 +0,0 @@
|
||||
|
||||
name: sinopia
|
||||
version: 0.1.1
|
||||
description: Private npm repository server
|
||||
|
||||
author:
|
||||
name: Alex Kocharin
|
||||
email: alex@kocharin.ru
|
||||
|
||||
main: index.js
|
||||
|
||||
bin:
|
||||
sinopia: ./bin/sinopia
|
||||
|
||||
dependencies:
|
||||
express: '>= 3.2.5'
|
||||
commander: '>= 1.1.1'
|
||||
js-yaml: '>= 2.0.5'
|
||||
cookies: '>= 0.3.6'
|
||||
request: '*'
|
||||
async: '*'
|
||||
semver: '*'
|
||||
|
||||
preferGlobal: true
|
||||
license: BSD
|
||||
|
||||
6
scripts/generate_authors.sh
Executable file
6
scripts/generate_authors.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
git shortlog -se \
|
||||
| perl -spe 's/^\s+\d+\s+//' \
|
||||
| sed -e '/^CommitSyncScript.*$/d' \
|
||||
> AUTHORS
|
||||
5
src/api/.eslintrc
Normal file
5
src/api/.eslintrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-useless-escape": 0
|
||||
}
|
||||
}
|
||||
94
src/api/endpoint/api/dist-tags.js
Normal file
94
src/api/endpoint/api/dist-tags.js
Normal file
@@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
|
||||
const Middleware = require('../../web/middleware');
|
||||
const mime = require('mime');
|
||||
const _ = require('lodash');
|
||||
|
||||
const media = Middleware.media;
|
||||
const expect_json = Middleware.expect_json;
|
||||
|
||||
module.exports = function(route, auth, storage) {
|
||||
const can = Middleware.allow(auth);
|
||||
const tag_package_version = function(req, res, next) {
|
||||
if (_.isString(req.body) === false) {
|
||||
return next('route');
|
||||
}
|
||||
|
||||
let tags = {};
|
||||
tags[req.params.tag] = req.body;
|
||||
storage.merge_tags(req.params.package, tags, function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.status(201);
|
||||
return next({ok: 'package tagged'});
|
||||
});
|
||||
};
|
||||
|
||||
// tagging a package
|
||||
route.put('/:package/:tag',
|
||||
can('publish'), media(mime.lookup('json')), tag_package_version);
|
||||
|
||||
route.post('/-/package/:package/dist-tags/:tag',
|
||||
can('publish'), media(mime.lookup('json')), tag_package_version);
|
||||
|
||||
route.put('/-/package/:package/dist-tags/:tag',
|
||||
can('publish'), media(mime.lookup('json')), tag_package_version);
|
||||
|
||||
route.delete('/-/package/:package/dist-tags/:tag', can('publish'), function(req, res, next) {
|
||||
const tags = {};
|
||||
tags[req.params.tag] = null;
|
||||
storage.merge_tags(req.params.package, tags, function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.status(201);
|
||||
return next({
|
||||
ok: 'tag removed',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
route.get('/-/package/:package/dist-tags', can('access'), function(req, res, next) {
|
||||
storage.get_package(req.params.package, {req: req}, function(err, info) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next(info['dist-tags']);
|
||||
});
|
||||
});
|
||||
|
||||
route.post('/-/package/:package/dist-tags', can('publish'), media(mime.lookup('json')), expect_json,
|
||||
function(req, res, next) {
|
||||
storage.merge_tags(req.params.package, req.body, function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.status(201);
|
||||
return next({ok: 'tags updated'});
|
||||
});
|
||||
});
|
||||
|
||||
route.put('/-/package/:package/dist-tags', can('publish'), media(mime.lookup('json')), expect_json,
|
||||
function(req, res, next) {
|
||||
storage.replace_tags(req.params.package, req.body, function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.status(201);
|
||||
return next({ok: 'tags updated'});
|
||||
});
|
||||
});
|
||||
|
||||
route.delete('/-/package/:package/dist-tags', can('publish'), media(mime.lookup('json')),
|
||||
function(req, res, next) {
|
||||
storage.replace_tags(req.params.package, {}, function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.status(201);
|
||||
return next({ok: 'tags removed'});
|
||||
});
|
||||
});
|
||||
};
|
||||
54
src/api/endpoint/api/package.js
Normal file
54
src/api/endpoint/api/package.js
Normal file
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const createError = require('http-errors');
|
||||
|
||||
const Middleware = require('../../web/middleware');
|
||||
const Utils = require('../../../lib/utils');
|
||||
|
||||
module.exports = function(route, auth, storage, config) {
|
||||
const can = Middleware.allow(auth);
|
||||
// TODO: anonymous user?
|
||||
route.get('/:package/:version?', can('access'), function(req, res, next) {
|
||||
storage.get_package(req.params.package, {req: req}, function(err, info) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
info = Utils.filter_tarball_urls(info, req, config);
|
||||
|
||||
let queryVersion = req.params.version;
|
||||
if (_.isNil(queryVersion)) {
|
||||
return next(info);
|
||||
}
|
||||
|
||||
let t = Utils.get_version(info, queryVersion);
|
||||
if (_.isNil(t) === false) {
|
||||
return next(t);
|
||||
}
|
||||
|
||||
if (_.isNil(info['dist-tags']) === false) {
|
||||
if (_.isNil(info['dist-tags'][queryVersion]) === false) {
|
||||
queryVersion = info['dist-tags'][queryVersion];
|
||||
t = Utils.get_version(info, queryVersion);
|
||||
if (_.isNil(t)) {
|
||||
return next(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next( createError[404]('version not found: ' + req.params.version) );
|
||||
});
|
||||
});
|
||||
|
||||
route.get('/:package/-/:filename', can('access'), function(req, res) {
|
||||
const stream = storage.get_tarball(req.params.package, req.params.filename);
|
||||
stream.on('content-length', function(content) {
|
||||
res.header('Content-Length', content);
|
||||
});
|
||||
stream.on('error', function(err) {
|
||||
return res.report_error(err);
|
||||
});
|
||||
res.header('Content-Type', 'application/octet-stream');
|
||||
stream.pipe(res);
|
||||
});
|
||||
};
|
||||
7
src/api/endpoint/api/ping.js
Normal file
7
src/api/endpoint/api/ping.js
Normal file
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function(route) {
|
||||
route.get('/-/ping', function(req, res, next) {
|
||||
next({});
|
||||
});
|
||||
};
|
||||
188
src/api/endpoint/api/publish.js
Normal file
188
src/api/endpoint/api/publish.js
Normal file
@@ -0,0 +1,188 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const Path = require('path');
|
||||
const createError = require('http-errors');
|
||||
|
||||
const Middleware = require('../../web/middleware');
|
||||
const Notify = require('../../../lib/notify');
|
||||
const Utils = require('../../../lib/utils');
|
||||
const mime = require('mime');
|
||||
|
||||
const media = Middleware.media;
|
||||
const expect_json = Middleware.expect_json;
|
||||
const notify = Notify.notify;
|
||||
|
||||
module.exports = function(router, auth, storage, config) {
|
||||
const can = Middleware.allow(auth);
|
||||
|
||||
// publishing a package
|
||||
router.put('/:package/:_rev?/:revision?', can('publish'), media(mime.lookup('json')), expect_json, function(req, res, next) {
|
||||
const name = req.params.package;
|
||||
let metadata;
|
||||
const create_tarball = function(filename, data, cb) {
|
||||
let stream = storage.add_tarball(name, filename);
|
||||
stream.on('error', function(err) {
|
||||
cb(err);
|
||||
});
|
||||
stream.on('success', function() {
|
||||
cb();
|
||||
});
|
||||
|
||||
// this is dumb and memory-consuming, but what choices do we have?
|
||||
stream.end(new Buffer(data.data, 'base64'));
|
||||
stream.done();
|
||||
};
|
||||
|
||||
const create_version = function(version, data, cb) {
|
||||
storage.add_version(name, version, data, null, cb);
|
||||
};
|
||||
|
||||
const add_tags = function(tags, cb) {
|
||||
storage.merge_tags(name, tags, cb);
|
||||
};
|
||||
|
||||
const after_change = function(err, ok_message) {
|
||||
// old npm behaviour
|
||||
if (_.isNil(metadata._attachments)) {
|
||||
if (err) return next(err);
|
||||
res.status(201);
|
||||
return next({
|
||||
ok: ok_message,
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
|
||||
// npm-registry-client 0.3+ embeds tarball into the json upload
|
||||
// https://github.com/isaacs/npm-registry-client/commit/e9fbeb8b67f249394f735c74ef11fe4720d46ca0
|
||||
// issue https://github.com/rlidwka/sinopia/issues/31, dealing with it here:
|
||||
|
||||
if (typeof(metadata._attachments) !== 'object'
|
||||
|| Object.keys(metadata._attachments).length !== 1
|
||||
|| typeof(metadata.versions) !== 'object'
|
||||
|| Object.keys(metadata.versions).length !== 1) {
|
||||
// npm is doing something strange again
|
||||
// if this happens in normal circumstances, report it as a bug
|
||||
return next( createError[400]('unsupported registry call') );
|
||||
}
|
||||
|
||||
if (err && err.status != 409) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// at this point document is either created or existed before
|
||||
const t1 = Object.keys(metadata._attachments)[0];
|
||||
create_tarball(Path.basename(t1), metadata._attachments[t1], function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const t2 = Object.keys(metadata.versions)[0];
|
||||
metadata.versions[t2].readme = _.isNil(metadata.readme) === false ? String(metadata.readme) : '';
|
||||
create_version(t2, metadata.versions[t2], function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
add_tags(metadata['dist-tags'], function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
notify(metadata, config);
|
||||
res.status(201);
|
||||
return next({ok: ok_message, success: true});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (Object.keys(req.body).length === 1 && Utils.is_object(req.body.users)) {
|
||||
// 501 status is more meaningful, but npm doesn't show error message for 5xx
|
||||
return next( createError[404]('npm star|unstar calls are not implemented') );
|
||||
}
|
||||
|
||||
try {
|
||||
metadata = Utils.validate_metadata(req.body, name);
|
||||
} catch(err) {
|
||||
return next( createError[422]('bad incoming package data') );
|
||||
}
|
||||
|
||||
if (req.params._rev) {
|
||||
storage.change_package(name, metadata, req.params.revision, function(err) {
|
||||
after_change(err, 'package changed');
|
||||
});
|
||||
} else {
|
||||
storage.addPackage(name, metadata, function(err) {
|
||||
after_change(err, 'created new package');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// unpublishing an entire package
|
||||
router.delete('/:package/-rev/*', can('publish'), function(req, res, next) {
|
||||
storage.remove_package(req.params.package, function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.status(201);
|
||||
return next({ok: 'package removed'});
|
||||
});
|
||||
});
|
||||
|
||||
// removing a tarball
|
||||
router.delete('/:package/-/:filename/-rev/:revision', can('publish'), function(req, res, next) {
|
||||
storage.remove_tarball(req.params.package, req.params.filename, req.params.revision, function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.status(201);
|
||||
return next({ok: 'tarball removed'});
|
||||
});
|
||||
});
|
||||
|
||||
// uploading package tarball
|
||||
router.put('/:package/-/:filename/*', can('publish'), media('application/octet-stream'), function(req, res, next) {
|
||||
const name = req.params.package;
|
||||
const stream = storage.add_tarball(name, req.params.filename);
|
||||
req.pipe(stream);
|
||||
|
||||
// checking if end event came before closing
|
||||
let complete = false;
|
||||
req.on('end', function() {
|
||||
complete = true;
|
||||
stream.done();
|
||||
});
|
||||
req.on('close', function() {
|
||||
if (!complete) {
|
||||
stream.abort();
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', function(err) {
|
||||
return res.report_error(err);
|
||||
});
|
||||
stream.on('success', function() {
|
||||
res.status(201);
|
||||
return next({
|
||||
ok: 'tarball uploaded successfully',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// adding a version
|
||||
router.put('/:package/:version/-tag/:tag', can('publish'), media(mime.lookup('json')), expect_json, function(req, res, next) {
|
||||
let name = req.params.package;
|
||||
let version = req.params.version;
|
||||
let tag = req.params.tag;
|
||||
|
||||
storage.add_version(name, version, req.body, tag, function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.status(201);
|
||||
return next({
|
||||
ok: 'package published',
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
99
src/api/endpoint/api/search.js
Normal file
99
src/api/endpoint/api/search.js
Normal file
@@ -0,0 +1,99 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function(route, auth, storage) {
|
||||
// searching packages
|
||||
route.get('/-/all(\/since)?', function(req, res) {
|
||||
let received_end = false;
|
||||
let response_finished = false;
|
||||
let processing_pkgs = 0;
|
||||
let firstPackage = true;
|
||||
|
||||
res.status(200);
|
||||
|
||||
/*
|
||||
* Offical NPM registry (registry.npmjs.org) no longer return whole database,
|
||||
* They only return packages matched with keyword in `referer: search pkg-name`,
|
||||
* And NPM client will request server in every search.
|
||||
*
|
||||
* The magic number 99999 was sent by NPM registry. Modify it may caused strange
|
||||
* behaviour in the future.
|
||||
*
|
||||
* BTW: NPM will not return result if user-agent does not contain string 'npm',
|
||||
* See: method 'request' in up-storage.js
|
||||
*
|
||||
* If there is no cache in local, NPM will request /-/all, then get response with
|
||||
* _updated: 99999, 'Date' in response header was Mon, 10 Oct 1983 00:12:48 GMT,
|
||||
* this will make NPM always query from server
|
||||
*
|
||||
* Data structure also different, whel request /-/all, response is an object, but
|
||||
* when request /-/all/since, response is an array
|
||||
*/
|
||||
const respShouldBeArray = req.path.endsWith('/since');
|
||||
res.set('Date', 'Mon, 10 Oct 1983 00:12:48 GMT');
|
||||
const check_finish = function() {
|
||||
if (!received_end) {
|
||||
return;
|
||||
}
|
||||
if (processing_pkgs) {
|
||||
return;
|
||||
}
|
||||
if (response_finished) {
|
||||
return;
|
||||
}
|
||||
response_finished = true;
|
||||
if (respShouldBeArray) {
|
||||
res.end(']\n');
|
||||
} else {
|
||||
res.end('}\n');
|
||||
}
|
||||
};
|
||||
|
||||
if (respShouldBeArray) {
|
||||
res.write('[');
|
||||
} else {
|
||||
res.write('{"_updated":' + 99999);
|
||||
}
|
||||
|
||||
let stream = storage.search(req.query.startkey || 0, {req: req});
|
||||
|
||||
stream.on('data', function each(pkg) {
|
||||
processing_pkgs++;
|
||||
|
||||
auth.allow_access(pkg.name, req.remote_user, function(err, allowed) {
|
||||
processing_pkgs--;
|
||||
|
||||
if (err) {
|
||||
if (err.status && String(err.status).match(/^4\d\d$/)) {
|
||||
// auth plugin returns 4xx user error,
|
||||
// that's equivalent of !allowed basically
|
||||
allowed = false;
|
||||
} else {
|
||||
stream.abort(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (allowed) {
|
||||
if (respShouldBeArray) {
|
||||
res.write(`${firstPackage ? '' : ','}${JSON.stringify(pkg)}\n`);
|
||||
if (firstPackage) {
|
||||
firstPackage = false;
|
||||
}
|
||||
} else {
|
||||
res.write(',\n' + JSON.stringify(pkg.name) + ':' + JSON.stringify(pkg));
|
||||
}
|
||||
}
|
||||
|
||||
check_finish();
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('error', function(_err) {
|
||||
res.socket.destroy();
|
||||
});
|
||||
|
||||
stream.on('end', function() {
|
||||
received_end = true;
|
||||
check_finish();
|
||||
});
|
||||
});
|
||||
};
|
||||
70
src/api/endpoint/api/user.js
Normal file
70
src/api/endpoint/api/user.js
Normal file
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const Cookies = require('cookies');
|
||||
const createError = require('http-errors');
|
||||
|
||||
module.exports = function(route, auth) {
|
||||
route.get('/-/user/:org_couchdb_user', function(req, res, next) {
|
||||
res.status(200);
|
||||
next({
|
||||
ok: 'you are authenticated as "' + req.remote_user.name + '"',
|
||||
});
|
||||
});
|
||||
|
||||
route.put('/-/user/:org_couchdb_user/:_rev?/:revision?', function(req, res, next) {
|
||||
let token = (req.body.name && req.body.password)
|
||||
? auth.aes_encrypt(req.body.name + ':' + req.body.password).toString('base64')
|
||||
: undefined;
|
||||
if (_.isNil(req.remote_user.name) === false) {
|
||||
res.status(201);
|
||||
return next({
|
||||
ok: 'you are authenticated as \'' + req.remote_user.name + '\'',
|
||||
// token: auth.issue_token(req.remote_user),
|
||||
token: token,
|
||||
});
|
||||
} else {
|
||||
auth.add_user(req.body.name, req.body.password, function(err, user) {
|
||||
if (err) {
|
||||
if (err.status >= 400 && err.status < 500) {
|
||||
// With npm registering is the same as logging in,
|
||||
// and npm accepts only an 409 error.
|
||||
// So, changing status code here.
|
||||
return next( createError[409](err.message) );
|
||||
}
|
||||
return next(err);
|
||||
}
|
||||
|
||||
req.remote_user = user;
|
||||
res.status(201);
|
||||
return next({
|
||||
ok: 'user \'' + req.body.name + '\' created',
|
||||
// token: auth.issue_token(req.remote_user),
|
||||
token: token,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
route.delete('/-/user/token/*', function(req, res, next) {
|
||||
res.status(200);
|
||||
next({
|
||||
ok: 'Logged out',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// placeholder 'cause npm require to be authenticated to publish
|
||||
// we do not do any real authentication yet
|
||||
route.post('/_session', Cookies.express(), function(req, res, next) {
|
||||
res.cookies.set('AuthSession', String(Math.random()), {
|
||||
// npmjs.org sets 10h expire
|
||||
expires: new Date(Date.now() + 10 * 60 * 60 * 1000),
|
||||
});
|
||||
next({
|
||||
ok: true,
|
||||
name: 'somebody',
|
||||
roles: [],
|
||||
});
|
||||
});
|
||||
};
|
||||
15
src/api/endpoint/api/whoami.js
Normal file
15
src/api/endpoint/api/whoami.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function(route) {
|
||||
route.get('/whoami', function(req, res, next) {
|
||||
if (req.headers.referer === 'whoami') {
|
||||
next({username: req.remote_user.name});
|
||||
} else {
|
||||
next('route');
|
||||
}
|
||||
});
|
||||
|
||||
route.get('/-/whoami', function(req, res, next) {
|
||||
next({username: req.remote_user.name});
|
||||
});
|
||||
};
|
||||
63
src/api/endpoint/index.js
Normal file
63
src/api/endpoint/index.js
Normal file
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const Middleware = require('../web/middleware');
|
||||
const match = Middleware.match;
|
||||
const validate_name = Middleware.validate_name;
|
||||
const validate_pkg = Middleware.validate_package;
|
||||
const encodeScopePackage = Middleware.encodeScopePackage;
|
||||
|
||||
const whoami = require('./api/whoami');
|
||||
const ping = require('./api/ping');
|
||||
const user = require('./api/user');
|
||||
const distTags = require('./api/dist-tags');
|
||||
const publish = require('./api/publish');
|
||||
const search = require('./api/search');
|
||||
const pkg = require('./api/package');
|
||||
|
||||
module.exports = function(config, auth, storage) {
|
||||
/* eslint new-cap:off */
|
||||
const app = express.Router();
|
||||
/* eslint new-cap:off */
|
||||
|
||||
// validate all of these params as a package name
|
||||
// this might be too harsh, so ask if it causes trouble
|
||||
app.param('package', validate_pkg);
|
||||
app.param('filename', validate_name);
|
||||
app.param('tag', validate_name);
|
||||
app.param('version', validate_name);
|
||||
app.param('revision', validate_name);
|
||||
app.param('token', validate_name);
|
||||
|
||||
// these can't be safely put into express url for some reason
|
||||
// TODO: For some reason? what reason?
|
||||
app.param('_rev', match(/^-rev$/));
|
||||
app.param('org_couchdb_user', match(/^org\.couchdb\.user:/));
|
||||
app.param('anything', match(/.*/));
|
||||
|
||||
app.use(auth.basic_middleware());
|
||||
// app.use(auth.bearer_middleware())
|
||||
app.use(bodyParser.json({strict: false, limit: config.max_body_size || '10mb'}));
|
||||
app.use(Middleware.anti_loop(config));
|
||||
|
||||
// encode / in a scoped package name to be matched as a single parameter in routes
|
||||
app.use(encodeScopePackage);
|
||||
|
||||
// for "npm whoami"
|
||||
whoami(app);
|
||||
|
||||
pkg(app, auth, storage, config);
|
||||
|
||||
search(app, auth, storage);
|
||||
|
||||
user(app, auth);
|
||||
|
||||
distTags(app, auth, storage);
|
||||
|
||||
publish(app, auth, storage, config);
|
||||
|
||||
ping(app);
|
||||
|
||||
return app;
|
||||
};
|
||||
123
src/api/index.js
Normal file
123
src/api/index.js
Normal file
@@ -0,0 +1,123 @@
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const Error = require('http-errors');
|
||||
const compression = require('compression');
|
||||
const Auth = require('../lib/auth');
|
||||
const Logger = require('../lib/logger');
|
||||
const Config = require('../lib/config');
|
||||
const Middleware = require('./web/middleware');
|
||||
const Cats = require('../lib/status-cats');
|
||||
const Storage = require('../lib/storage');
|
||||
const _ = require('lodash');
|
||||
const cors = require('cors');
|
||||
|
||||
module.exports = function(config_hash) {
|
||||
// Config
|
||||
Logger.setup(config_hash.logs);
|
||||
const config = new Config(config_hash);
|
||||
const storage = new Storage(config);
|
||||
const auth = new Auth(config);
|
||||
const app = express();
|
||||
// run in production mode by default, just in case
|
||||
// it shouldn't make any difference anyway
|
||||
app.set('env', process.env.NODE_ENV || 'production');
|
||||
app.use(cors());
|
||||
|
||||
// Middleware
|
||||
const error_reporting_middleware = function(req, res, next) {
|
||||
res.report_error = res.report_error || function(err) {
|
||||
if (err.status && err.status >= 400 && err.status < 600) {
|
||||
if (_.isNil(res.headersSent) === false) {
|
||||
res.status(err.status);
|
||||
next({error: err.message || 'unknown error'});
|
||||
}
|
||||
} else {
|
||||
Logger.logger.error( {err: err}
|
||||
, 'unexpected error: @{!err.message}\n@{err.stack}');
|
||||
if (!res.status || !res.send) {
|
||||
Logger.logger.error('this is an error in express.js, please report this');
|
||||
res.destroy();
|
||||
} else if (!res.headersSent) {
|
||||
res.status(500);
|
||||
next({error: 'internal server error'});
|
||||
} else {
|
||||
// socket should be already closed
|
||||
}
|
||||
}
|
||||
};
|
||||
next();
|
||||
};
|
||||
|
||||
// Router setup
|
||||
app.use(Middleware.log);
|
||||
app.use(error_reporting_middleware);
|
||||
app.use(function(req, res, next) {
|
||||
res.setHeader('X-Powered-By', config.user_agent);
|
||||
next();
|
||||
});
|
||||
app.use(Cats.middleware);
|
||||
app.use(compression());
|
||||
|
||||
app.get('/favicon.ico', function(req, res, next) {
|
||||
req.url = '/-/static/favicon.png';
|
||||
next();
|
||||
});
|
||||
|
||||
// Hook for tests only
|
||||
if (config._debug) {
|
||||
app.get('/-/_debug', function(req, res, next) {
|
||||
const do_gc = _.isNil(global.gc) === false;
|
||||
if (do_gc) {
|
||||
global.gc();
|
||||
}
|
||||
next({
|
||||
pid: process.pid,
|
||||
main: process.mainModule.filename,
|
||||
conf: config.self_path,
|
||||
mem: process.memoryUsage(),
|
||||
gc: do_gc,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// For npm request
|
||||
app.use(require('./endpoint')(config, auth, storage));
|
||||
|
||||
// For WebUI & WebUI API
|
||||
if (_.get(config, 'web.enable', true)) {
|
||||
app.use('/', require('./web')(config, auth, storage));
|
||||
app.use('/-/verdaccio/', require('./web/api')(config, auth, storage));
|
||||
} else {
|
||||
app.get('/', function(req, res, next) {
|
||||
next(Error[404]('Web interface is disabled in the config file'));
|
||||
});
|
||||
}
|
||||
|
||||
// Catch 404
|
||||
app.get('/*', function(req, res, next) {
|
||||
next(Error[404]('File not found'));
|
||||
});
|
||||
|
||||
app.use(function(err, req, res, next) {
|
||||
if (_.isError(err)) {
|
||||
if (err.code === 'ECONNABORT' && res.statusCode === 304) {
|
||||
return next();
|
||||
}
|
||||
if (_.isFunction(res.report_error) === false) {
|
||||
// in case of very early error this middleware may not be loaded before error is generated
|
||||
// fixing that
|
||||
error_reporting_middleware(req, res, _.noop);
|
||||
}
|
||||
res.report_error(err);
|
||||
} else {
|
||||
// Fall to Middleware.final
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
|
||||
app.use(Middleware.final);
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
147
src/api/web/api.js
Normal file
147
src/api/web/api.js
Normal file
@@ -0,0 +1,147 @@
|
||||
'use strict';
|
||||
|
||||
const bodyParser = require('body-parser');
|
||||
const express = require('express');
|
||||
const marked = require('marked');
|
||||
const Search = require('../../lib/search');
|
||||
const Middleware = require('./middleware');
|
||||
const match = Middleware.match;
|
||||
const validateName = Middleware.validate_name;
|
||||
const validatePkg = Middleware.validate_package;
|
||||
const securityIframe = Middleware.securityIframe;
|
||||
const route = express.Router(); // eslint-disable-line
|
||||
const async = require('async');
|
||||
const HTTPError = require('http-errors');
|
||||
const Utils = require('../../lib/utils');
|
||||
|
||||
/*
|
||||
This file include all verdaccio only API(Web UI), for npm API please see ../endpoint/
|
||||
*/
|
||||
|
||||
module.exports = function(config, auth, storage) {
|
||||
Search.configureStorage(storage);
|
||||
const can = Middleware.allow(auth);
|
||||
|
||||
// validate all of these params as a package name
|
||||
// this might be too harsh, so ask if it causes trouble
|
||||
route.param('package', validatePkg);
|
||||
route.param('filename', validateName);
|
||||
route.param('version', validateName);
|
||||
route.param('anything', match(/.*/));
|
||||
|
||||
route.use(bodyParser.urlencoded({extended: false}));
|
||||
route.use(auth.jwtMiddleware());
|
||||
route.use(securityIframe);
|
||||
|
||||
// Get list of all visible package
|
||||
route.get('/packages', function(req, res, next) {
|
||||
storage.get_local(function(err, packages) {
|
||||
if (err) {
|
||||
// that function shouldn't produce any
|
||||
throw err;
|
||||
}
|
||||
|
||||
async.filterSeries(
|
||||
packages,
|
||||
function(pkg, cb) {
|
||||
auth.allow_access(pkg.name, req.remote_user, function(err, allowed) {
|
||||
setImmediate(function() {
|
||||
if (err) {
|
||||
cb(null, false);
|
||||
} else {
|
||||
cb(err, allowed);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
function(err, packages) {
|
||||
if (err) throw err;
|
||||
|
||||
packages.sort(function(a, b) {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
next(packages);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Get package readme
|
||||
route.get('/package/readme(/@:scope?)?/:package/:version?', can('access'), function(req, res, next) {
|
||||
let packageName = req.params.package;
|
||||
if (req.params.scope) {
|
||||
packageName = `@${req.params.scope}/${packageName}`;
|
||||
}
|
||||
storage.get_package(packageName, {req: req}, function(err, info) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.set('Content-Type', 'text/plain');
|
||||
next(marked(info.readme || 'ERROR: No README data found!'));
|
||||
});
|
||||
});
|
||||
|
||||
// Search package
|
||||
route.get('/search/:anything', function(req, res, next) {
|
||||
const results = Search.query(req.params.anything);
|
||||
const packages = [];
|
||||
|
||||
const getPackageInfo = function(i) {
|
||||
storage.get_package(results[i].ref, (err, entry) => {
|
||||
if (!err && entry) {
|
||||
auth.allow_access(entry.name, req.remote_user, function(err, allowed) {
|
||||
if (err || !allowed) {
|
||||
return;
|
||||
}
|
||||
|
||||
packages.push(entry.versions[entry['dist-tags'].latest]);
|
||||
});
|
||||
}
|
||||
|
||||
if (i >= results.length - 1) {
|
||||
next(packages);
|
||||
} else {
|
||||
getPackageInfo(i + 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (results.length) {
|
||||
getPackageInfo(0);
|
||||
} else {
|
||||
next([]);
|
||||
}
|
||||
});
|
||||
|
||||
route.post('/login', function(req, res, next) {
|
||||
auth.authenticate(req.body.username, req.body.password, (err, user) => {
|
||||
if (!err) {
|
||||
req.remote_user = user;
|
||||
|
||||
next({
|
||||
token: auth.issue_token(user, '24h'),
|
||||
username: req.remote_user.name,
|
||||
});
|
||||
} else {
|
||||
next(HTTPError[err.message ? 401 : 500](err.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
route.post('/-/logout', function(req, res, next) {
|
||||
let base = Utils.combineBaseUrl(Utils.getWebProtocol(req), req.get('host'), config.url_prefix);
|
||||
res.cookies.set('token', '');
|
||||
res.redirect(base);
|
||||
});
|
||||
|
||||
// What are you looking for? logout? client side will remove token when user click logout,
|
||||
// or it will auto expire after 24 hours.
|
||||
// This token is different with the token send to npm client.
|
||||
// We will/may replace current token with JWT in next major release, and it will not expire at all(configurable).
|
||||
|
||||
return route;
|
||||
};
|
||||
53
src/api/web/index.js
Normal file
53
src/api/web/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const Search = require('../../lib/search');
|
||||
const Middleware = require('./middleware');
|
||||
const Utils = require('../../lib/utils');
|
||||
/* eslint new-cap:off */
|
||||
const router = express.Router();
|
||||
const _ = require('lodash');
|
||||
const env = require('../../config/env');
|
||||
const fs = require('fs');
|
||||
const template = fs.readFileSync(`${env.DIST_PATH}/index.html`).toString();
|
||||
|
||||
module.exports = function(config, auth, storage) {
|
||||
Search.configureStorage(storage);
|
||||
|
||||
router.use(auth.jwtMiddleware());
|
||||
router.use(Middleware.securityIframe);
|
||||
|
||||
// Static
|
||||
router.get('/-/static/:filename', function(req, res, next) {
|
||||
const file = `${env.APP_ROOT}/static/${req.params.filename}`;
|
||||
res.sendFile(file, function(err) {
|
||||
if (!err) {
|
||||
return;
|
||||
}
|
||||
if (err.status === 404) {
|
||||
next();
|
||||
} else {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/-/verdaccio/logo', function(req, res) {
|
||||
res.send(_.get(config, 'web.logo') || '/-/static/logo.png');
|
||||
});
|
||||
|
||||
router.get('/', function(req, res) {
|
||||
const base = Utils.combineBaseUrl(Utils.getWebProtocol(req), req.get('host'), config.url_prefix);
|
||||
const defaultTitle = 'Verdaccio';
|
||||
let webPage = template
|
||||
.replace(/ToReplaceByVerdaccio/g, base)
|
||||
.replace(/ToReplaceByTitle/g, _.get(config, 'web.title') ? config.web.title : defaultTitle)
|
||||
.replace(/(main.*\.js|style.*\.css)/g, `${base}/-/static/$1`);
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
|
||||
res.send(webPage);
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
253
src/api/web/middleware.js
Normal file
253
src/api/web/middleware.js
Normal file
@@ -0,0 +1,253 @@
|
||||
/* eslint prefer-rest-params: "off" */
|
||||
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const _ = require('lodash');
|
||||
const createError = require('http-errors');
|
||||
const utils = require('../../lib/utils');
|
||||
const Logger = require('../../lib/logger');
|
||||
|
||||
|
||||
module.exports.match = function match(regexp) {
|
||||
return function(req, res, next, value) {
|
||||
if (regexp.exec(value)) {
|
||||
next();
|
||||
} else {
|
||||
next('route');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.securityIframe = function securityIframe(req, res, next) {
|
||||
// disable loading in frames (clickjacking, etc.)
|
||||
res.header('X-Frame-Options', 'deny');
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports.validate_name = function validate_name(req, res, next, value, name) {
|
||||
if (value.charAt(0) === '-') {
|
||||
// special case in couchdb usually
|
||||
next('route');
|
||||
} else if (utils.validate_name(value)) {
|
||||
next();
|
||||
} else {
|
||||
next( createError[403]('invalid ' + name) );
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.validate_package = function validate_package(req, res, next, value, name) {
|
||||
if (value.charAt(0) === '-') {
|
||||
// special case in couchdb usually
|
||||
next('route');
|
||||
} else if (utils.validate_package(value)) {
|
||||
next();
|
||||
} else {
|
||||
next( createError[403]('invalid ' + name) );
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.media = function media(expect) {
|
||||
return function(req, res, next) {
|
||||
if (req.headers['content-type'] !== expect) {
|
||||
next( createError[415]('wrong content-type, expect: ' + expect
|
||||
+ ', got: '+req.headers['content-type']) );
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.encodeScopePackage = function(req, res, next) {
|
||||
if (req.url.indexOf('@') !== -1) {
|
||||
// e.g.: /@org/pkg/1.2.3 -> /@org%2Fpkg/1.2.3, /@org%2Fpkg/1.2.3 -> /@org%2Fpkg/1.2.3
|
||||
req.url = req.url.replace(/^(\/@[^\/%]+)\/(?!$)/, '$1%2F');
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports.expect_json = function expect_json(req, res, next) {
|
||||
if (!utils.is_object(req.body)) {
|
||||
return next( createError[400]('can\'t parse incoming json') );
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports.anti_loop = function(config) {
|
||||
return function(req, res, next) {
|
||||
if (req.headers.via != null) {
|
||||
let arr = req.headers.via.split(',');
|
||||
|
||||
for (let i=0; i<arr.length; i++) {
|
||||
let m = arr[i].match(/\s*(\S+)\s+(\S+)/);
|
||||
if (m && m[2] === config.server_id) {
|
||||
return next( createError[508]('loop detected') );
|
||||
}
|
||||
}
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Express doesn't do etags with requests <= 1024b
|
||||
* we use md5 here, it works well on 1k+ bytes, but sucks with fewer data
|
||||
* could improve performance using crc32 after benchmarks.
|
||||
* @param {Object} data
|
||||
* @return {String}
|
||||
*/
|
||||
function md5sum(data) {
|
||||
return crypto.createHash('md5').update(data).digest('hex');
|
||||
}
|
||||
|
||||
|
||||
module.exports.allow = function(auth) {
|
||||
return function(action) {
|
||||
return function(req, res, next) {
|
||||
req.pause();
|
||||
let packageName = req.params.package;
|
||||
if (req.params.scope) {
|
||||
packageName = `@${req.params.scope}/${packageName}`;
|
||||
}
|
||||
|
||||
auth['allow_' + action](packageName, req.remote_user, function(error, allowed) {
|
||||
req.resume();
|
||||
if (error) {
|
||||
next(error);
|
||||
} else if (allowed) {
|
||||
next();
|
||||
} else {
|
||||
// last plugin (that's our built-in one) returns either
|
||||
// cb(err) or cb(null, true), so this should never happen
|
||||
throw createError('bug in the auth plugin system');
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.final = function(body, req, res, next) {
|
||||
if (res.statusCode === 401 && !res.getHeader('WWW-Authenticate')) {
|
||||
// they say it's required for 401, so...
|
||||
res.header('WWW-Authenticate', 'Basic, Bearer');
|
||||
}
|
||||
|
||||
try {
|
||||
if (_.isString(body) || _.isObject(body)) {
|
||||
if (!res.getHeader('Content-type')) {
|
||||
res.header('Content-type', 'application/json');
|
||||
}
|
||||
|
||||
if (typeof(body) === 'object' && _.isNil(body) === false) {
|
||||
if (typeof(body.error) === 'string') {
|
||||
res._verdaccio_error = body.error;
|
||||
}
|
||||
body = JSON.stringify(body, undefined, ' ') + '\n';
|
||||
}
|
||||
|
||||
// don't send etags with errors
|
||||
if (!res.statusCode || (res.statusCode >= 200 && res.statusCode < 300)) {
|
||||
res.header('ETag', '"' + md5sum(body) + '"');
|
||||
}
|
||||
} else {
|
||||
// send(null), send(204), etc.
|
||||
}
|
||||
} catch(err) {
|
||||
// if verdaccio sends headers first, and then calls res.send()
|
||||
// as an error handler, we can't report error properly,
|
||||
// and should just close socket
|
||||
if (err.message.match(/set headers after they are sent/)) {
|
||||
if (_.isNil(res.socket) === false) {
|
||||
res.socket.destroy();
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
res.send(body);
|
||||
};
|
||||
|
||||
module.exports.log = function(req, res, next) {
|
||||
// logger
|
||||
req.log = Logger.logger.child({sub: 'in'});
|
||||
|
||||
let _auth = req.headers.authorization;
|
||||
if (_.isNil(_auth) === false) {
|
||||
req.headers.authorization = '<Classified>';
|
||||
}
|
||||
let _cookie = req.headers.cookie;
|
||||
if (_.isNil(_cookie) === false) {
|
||||
req.headers.cookie = '<Classified>';
|
||||
}
|
||||
|
||||
req.url = req.originalUrl;
|
||||
req.log.info( {req: req, ip: req.ip}
|
||||
, '@{ip} requested \'@{req.method} @{req.url}\'' );
|
||||
req.originalUrl = req.url;
|
||||
|
||||
if (_.isNil(_auth) === false) {
|
||||
req.headers.authorization = _auth;
|
||||
}
|
||||
|
||||
if (_.isNil(_cookie) === false) {
|
||||
req.headers.cookie = _cookie;
|
||||
}
|
||||
|
||||
let bytesin = 0;
|
||||
req.on('data', function(chunk) {
|
||||
bytesin += chunk.length;
|
||||
});
|
||||
|
||||
let bytesout = 0;
|
||||
let _write = res.write;
|
||||
res.write = function(buf) {
|
||||
bytesout += buf.length;
|
||||
_write.apply(res, arguments);
|
||||
};
|
||||
|
||||
const log = function() {
|
||||
let forwardedFor = req.headers['x-forwarded-for'];
|
||||
let remoteAddress = req.connection.remoteAddress;
|
||||
let remoteIP = forwardedFor ? `${forwardedFor} via ${remoteAddress}` : remoteAddress;
|
||||
let message = '@{status}, user: @{user}(@{remoteIP}), req: \'@{request.method} @{request.url}\'';
|
||||
if (res._verdaccio_error) {
|
||||
message += ', error: @{!error}';
|
||||
} else {
|
||||
message += ', bytes: @{bytes.in}/@{bytes.out}';
|
||||
}
|
||||
|
||||
req.url = req.originalUrl;
|
||||
req.log.warn({
|
||||
request: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
},
|
||||
level: 35, // http
|
||||
user: req.remote_user && req.remote_user.name,
|
||||
remoteIP,
|
||||
status: res.statusCode,
|
||||
error: res._verdaccio_error,
|
||||
bytes: {
|
||||
in: bytesin,
|
||||
out: bytesout,
|
||||
},
|
||||
}, message);
|
||||
req.originalUrl = req.url;
|
||||
};
|
||||
|
||||
req.on('close', function() {
|
||||
log(true);
|
||||
});
|
||||
|
||||
const _end = res.end;
|
||||
res.end = function(buf) {
|
||||
if (buf) {
|
||||
bytesout += buf.length;
|
||||
}
|
||||
_end.apply(res, arguments);
|
||||
log();
|
||||
};
|
||||
next();
|
||||
};
|
||||
9
src/config/env.js
Normal file
9
src/config/env.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const path = require('path');
|
||||
|
||||
const APP_ROOT = path.resolve(__dirname, '../../');
|
||||
|
||||
module.exports = {
|
||||
APP_ROOT,
|
||||
SRC_ROOT: path.resolve(APP_ROOT, 'src/'),
|
||||
DIST_PATH: path.resolve(APP_ROOT, 'static/'),
|
||||
};
|
||||
5
src/lib/.eslintrc
Normal file
5
src/lib/.eslintrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-useless-escape": 0
|
||||
}
|
||||
}
|
||||
446
src/lib/auth.js
Normal file
446
src/lib/auth.js
Normal file
@@ -0,0 +1,446 @@
|
||||
/* eslint prefer-spread: "off" */
|
||||
/* eslint prefer-rest-params: "off" */
|
||||
|
||||
'use strict';
|
||||
|
||||
const Crypto = require('crypto');
|
||||
const Error = require('http-errors');
|
||||
const Logger = require('./logger');
|
||||
const load_plugins = require('./plugin-loader').load_plugins;
|
||||
const pkgJson = require('../../package.json');
|
||||
const jwt = require('jsonwebtoken');
|
||||
/**
|
||||
* Handles the authentification, load auth plugins.
|
||||
*/
|
||||
class Auth {
|
||||
|
||||
/**
|
||||
* @param {*} config config reference
|
||||
*/
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.logger = Logger.logger.child({sub: 'auth'});
|
||||
this.secret = config.secret;
|
||||
|
||||
const plugin_params = {
|
||||
config: config,
|
||||
logger: this.logger,
|
||||
};
|
||||
|
||||
if (config.users_file) {
|
||||
if (!config.auth || !config.auth.htpasswd) {
|
||||
// b/w compat
|
||||
config.auth = config.auth || {};
|
||||
config.auth.htpasswd = {file: config.users_file};
|
||||
}
|
||||
}
|
||||
|
||||
this.plugins = load_plugins(config, config.auth, plugin_params, function(p) {
|
||||
return p.authenticate || p.allow_access || p.allow_publish;
|
||||
});
|
||||
|
||||
this.plugins.unshift({
|
||||
verdaccio_version: pkgJson.version,
|
||||
|
||||
authenticate: function(user, password, cb) {
|
||||
if (config.users != null
|
||||
&& config.users[user] != null
|
||||
&& (Crypto.createHash('sha1').update(password).digest('hex')
|
||||
=== config.users[user].password)
|
||||
) {
|
||||
return cb(null, [user]);
|
||||
}
|
||||
|
||||
return cb();
|
||||
},
|
||||
|
||||
adduser: function(user, password, cb) {
|
||||
if (config.users && config.users[user]) {
|
||||
return cb(Error[403]('this user already exists'));
|
||||
}
|
||||
|
||||
return cb();
|
||||
},
|
||||
});
|
||||
|
||||
const allow_action = function(action) {
|
||||
return function(user, pkg, cb) {
|
||||
let ok = pkg[action].reduce(function(prev, curr) {
|
||||
if (user.name === curr || user.groups.indexOf(curr) !== -1) return true;
|
||||
return prev;
|
||||
}, false);
|
||||
|
||||
if (ok) return cb(null, true);
|
||||
|
||||
if (user.name) {
|
||||
cb( Error[403]('user ' + user.name + ' is not allowed to ' + action + ' package ' + pkg.name) );
|
||||
} else {
|
||||
cb( Error[403]('unregistered users are not allowed to ' + action + ' package ' + pkg.name) );
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
this.plugins.push({
|
||||
authenticate: function(user, password, cb) {
|
||||
return cb( Error[403]('bad username/password, access denied') );
|
||||
},
|
||||
|
||||
add_user: function(user, password, cb) {
|
||||
return cb( Error[409]('registration is disabled') );
|
||||
},
|
||||
|
||||
allow_access: allow_action('access'),
|
||||
allow_publish: allow_action('publish'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate an user.
|
||||
* @param {*} user
|
||||
* @param {*} password
|
||||
* @param {*} cb
|
||||
*/
|
||||
authenticate(user, password, cb) {
|
||||
const plugins = this.plugins.slice(0)
|
||||
;(function next() {
|
||||
let p = plugins.shift();
|
||||
|
||||
if (typeof(p.authenticate) !== 'function') {
|
||||
return next();
|
||||
}
|
||||
|
||||
p.authenticate(user, password, function(err, groups) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (groups != null && groups != false) {
|
||||
return cb(err, authenticatedUser(user, groups));
|
||||
}
|
||||
next();
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new user.
|
||||
* @param {*} user
|
||||
* @param {*} password
|
||||
* @param {*} cb
|
||||
*/
|
||||
add_user(user, password, cb) {
|
||||
let self = this;
|
||||
let plugins = this.plugins.slice(0)
|
||||
|
||||
;(function next() {
|
||||
let p = plugins.shift();
|
||||
let n = 'adduser';
|
||||
if (typeof(p[n]) !== 'function') {
|
||||
n = 'add_user';
|
||||
}
|
||||
if (typeof(p[n]) !== 'function') {
|
||||
next();
|
||||
} else {
|
||||
p[n](user, password, function(err, ok) {
|
||||
if (err) return cb(err);
|
||||
if (ok) return self.authenticate(user, password, cb);
|
||||
next();
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow user to access a package.
|
||||
* @param {*} package_name
|
||||
* @param {*} user
|
||||
* @param {*} callback
|
||||
*/
|
||||
allow_access(package_name, user, callback) {
|
||||
let plugins = this.plugins.slice(0);
|
||||
let pkg = Object.assign({name: package_name},
|
||||
this.config.getMatchedPackagesSpec(package_name))
|
||||
|
||||
;(function next() {
|
||||
let p = plugins.shift();
|
||||
|
||||
if (typeof(p.allow_access) !== 'function') {
|
||||
return next();
|
||||
}
|
||||
|
||||
p.allow_access(user, pkg, function(err, ok) {
|
||||
if (err) return callback(err);
|
||||
if (ok) return callback(null, ok);
|
||||
next(); // cb(null, false) causes next plugin to roll
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow user to publish a package.
|
||||
* @param {*} package_name
|
||||
* @param {*} user
|
||||
* @param {*} callback
|
||||
*/
|
||||
allow_publish(package_name, user, callback) {
|
||||
let plugins = this.plugins.slice(0);
|
||||
let pkg = Object.assign({name: package_name},
|
||||
this.config.getMatchedPackagesSpec(package_name))
|
||||
|
||||
;(function next() {
|
||||
let p = plugins.shift();
|
||||
|
||||
if (typeof(p.allow_publish) !== 'function') {
|
||||
return next();
|
||||
}
|
||||
|
||||
p.allow_publish(user, pkg, function(err, ok) {
|
||||
if (err) return callback(err);
|
||||
if (ok) return callback(null, ok);
|
||||
next(); // cb(null, false) causes next plugin to roll
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a basic middleware.
|
||||
* @return {Function}
|
||||
*/
|
||||
basic_middleware() {
|
||||
let self = this;
|
||||
let credentials;
|
||||
return function(req, res, _next) {
|
||||
req.pause();
|
||||
|
||||
const next = function(err) {
|
||||
req.resume();
|
||||
// uncomment this to reject users with bad auth headers
|
||||
// return _next.apply(null, arguments)
|
||||
|
||||
// swallow error, user remains unauthorized
|
||||
// set remoteUserError to indicate that user was attempting authentication
|
||||
if (err) {
|
||||
req.remote_user.error = err.message;
|
||||
}
|
||||
return _next();
|
||||
};
|
||||
|
||||
if (req.remote_user != null && req.remote_user.name !== undefined) {
|
||||
return next();
|
||||
}
|
||||
req.remote_user = buildAnonymousUser();
|
||||
|
||||
let authorization = req.headers.authorization;
|
||||
if (authorization == null) return next();
|
||||
|
||||
let parts = authorization.split(' ');
|
||||
|
||||
if (parts.length !== 2) {
|
||||
return next( Error[400]('bad authorization header') );
|
||||
}
|
||||
|
||||
const scheme = parts[0];
|
||||
if (scheme === 'Basic') {
|
||||
credentials = new Buffer(parts[1], 'base64').toString();
|
||||
} else if (scheme === 'Bearer') {
|
||||
credentials = self.aes_decrypt(new Buffer(parts[1], 'base64')).toString('utf8');
|
||||
if (!credentials) {
|
||||
return next();
|
||||
}
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
|
||||
const index = credentials.indexOf(':');
|
||||
if (index < 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const user = credentials.slice(0, index);
|
||||
const pass = credentials.slice(index + 1);
|
||||
|
||||
self.authenticate(user, pass, function(err, user) {
|
||||
if (!err) {
|
||||
req.remote_user = user;
|
||||
next();
|
||||
} else {
|
||||
req.remote_user = buildAnonymousUser();
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the bearer middleware.
|
||||
* @return {Function}
|
||||
*/
|
||||
bearer_middleware() {
|
||||
let self = this;
|
||||
return function(req, res, _next) {
|
||||
req.pause();
|
||||
const next = function(_err) {
|
||||
req.resume();
|
||||
return _next.apply(null, arguments);
|
||||
};
|
||||
|
||||
if (req.remote_user != null && req.remote_user.name !== undefined) {
|
||||
return next();
|
||||
}
|
||||
req.remote_user = buildAnonymousUser();
|
||||
|
||||
let authorization = req.headers.authorization;
|
||||
if (authorization == null) {
|
||||
return next();
|
||||
}
|
||||
|
||||
let parts = authorization.split(' ');
|
||||
|
||||
if (parts.length !== 2) {
|
||||
return next( Error[400]('bad authorization header') );
|
||||
}
|
||||
|
||||
let scheme = parts[0];
|
||||
let token = parts[1];
|
||||
|
||||
if (scheme !== 'Bearer') {
|
||||
return next();
|
||||
}
|
||||
let user;
|
||||
try {
|
||||
user = self.decode_token(token);
|
||||
} catch(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
req.remote_user = authenticatedUser(user.u, user.g);
|
||||
req.remote_user.token = token;
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT middleware for WebUI
|
||||
* @return {Function}
|
||||
*/
|
||||
jwtMiddleware() {
|
||||
return (req, res, _next) => {
|
||||
if (req.remote_user !== null && req.remote_user.name !== undefined) return _next();
|
||||
|
||||
req.pause();
|
||||
const next = function(_err) {
|
||||
req.resume();
|
||||
return _next();
|
||||
};
|
||||
|
||||
req.remote_user = buildAnonymousUser();
|
||||
|
||||
let token = (req.headers.authorization || '').replace('Bearer ', '');
|
||||
if (!token) return next();
|
||||
|
||||
let decoded;
|
||||
try {
|
||||
decoded = this.decode_token(token);
|
||||
} catch (err) {/**/}
|
||||
|
||||
if (decoded) {
|
||||
req.remote_user = authenticatedUser(decoded.user, decoded.group);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the token.
|
||||
* @param {object} user
|
||||
* @param {string} expire_time
|
||||
* @return {string}
|
||||
*/
|
||||
issue_token(user, expire_time) {
|
||||
return jwt.sign(
|
||||
{
|
||||
user: user.name,
|
||||
group: user.real_groups && user.real_groups.length ? user.real_groups : undefined,
|
||||
},
|
||||
this.secret,
|
||||
{
|
||||
notBefore: '1000', // Make sure the time will not rollback :)
|
||||
expiresIn: expire_time || '7d',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the token.
|
||||
* @param {*} token
|
||||
* @return {Object}
|
||||
*/
|
||||
decode_token(token) {
|
||||
let decoded;
|
||||
try {
|
||||
decoded = jwt.verify(token, this.secret);
|
||||
} catch (err) {
|
||||
throw Error[401](err.message);
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a string.
|
||||
* @param {String} buf
|
||||
* @return {Buffer}
|
||||
*/
|
||||
aes_encrypt(buf) {
|
||||
const c = Crypto.createCipher('aes192', this.secret);
|
||||
const b1 = c.update(buf);
|
||||
const b2 = c.final();
|
||||
return Buffer.concat([b1, b2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dencrypt a string.
|
||||
* @param {String} buf
|
||||
* @return {Buffer}
|
||||
*/
|
||||
aes_decrypt(buf) {
|
||||
try {
|
||||
const c = Crypto.createDecipher('aes192', this.secret);
|
||||
const b1 = c.update(buf);
|
||||
const b2 = c.final();
|
||||
return Buffer.concat([b1, b2]);
|
||||
} catch(_) {
|
||||
return new Buffer(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an anonymous user in case none is logged in.
|
||||
* @return {Object} { name: xx, groups: [], real_groups: [] }
|
||||
*/
|
||||
function buildAnonymousUser() {
|
||||
return {
|
||||
name: undefined,
|
||||
// groups without '$' are going to be deprecated eventually
|
||||
groups: ['$all', '$anonymous', '@all', '@anonymous', 'all', 'undefined', 'anonymous'],
|
||||
real_groups: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate an user.
|
||||
* @param {*} name
|
||||
* @param {*} groups
|
||||
* @return {Object} { name: xx, groups: [], real_groups: [] }
|
||||
*/
|
||||
function authenticatedUser(name, groups) {
|
||||
let _groups = (groups || []).concat(['$all', '$authenticated', '@all', '@authenticated', 'all']);
|
||||
return {
|
||||
name: name,
|
||||
groups: _groups,
|
||||
real_groups: groups,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Auth;
|
||||
199
src/lib/cli.js
Normal file
199
src/lib/cli.js
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/* eslint no-sync:0 */
|
||||
/* eslint no-empty:0 */
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
if (process.getuid && process.getuid() === 0) {
|
||||
global.console.error('Verdaccio doesn\'t need superuser privileges. Don\'t run it under root.');
|
||||
}
|
||||
|
||||
process.title = 'verdaccio';
|
||||
|
||||
try {
|
||||
// for debugging memory leaks
|
||||
// totally optional
|
||||
require('heapdump');
|
||||
} catch(err) { }
|
||||
|
||||
const logger = require('./logger');
|
||||
logger.setup(); // default setup
|
||||
|
||||
const commander = require('commander');
|
||||
const constants = require('constants');
|
||||
const fs = require('fs');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const Path = require('path');
|
||||
const URL = require('url');
|
||||
const server = require('../api/index');
|
||||
const Utils = require('./utils');
|
||||
const pkginfo = require('pkginfo')(module); // eslint-disable-line no-unused-vars
|
||||
const pkgVersion = module.exports.version;
|
||||
const pkgName = module.exports.name;
|
||||
|
||||
commander
|
||||
.option('-l, --listen <[host:]port>', 'host:port number to listen on (default: localhost:4873)')
|
||||
.option('-c, --config <config.yaml>', 'use this configuration file (default: ./config.yaml)')
|
||||
.version(pkgVersion)
|
||||
.parse(process.argv);
|
||||
|
||||
if (commander.args.length == 1 && !commander.config) {
|
||||
// handling "verdaccio [config]" case if "-c" is missing in commandline
|
||||
commander.config = commander.args.pop();
|
||||
}
|
||||
|
||||
if (commander.args.length != 0) {
|
||||
commander.help();
|
||||
}
|
||||
|
||||
let config;
|
||||
let config_path;
|
||||
try {
|
||||
if (commander.config) {
|
||||
config_path = Path.resolve(commander.config);
|
||||
} else {
|
||||
config_path = require('./config-path')();
|
||||
}
|
||||
config = Utils.parseConfigFile(config_path);
|
||||
logger.logger.warn({file: config_path}, 'config file - @{file}');
|
||||
} catch (err) {
|
||||
logger.logger.fatal({file: config_path, err: err}, 'cannot open config file @{file}: @{!err.message}');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
afterConfigLoad();
|
||||
|
||||
/**
|
||||
* Retrieve all addresses defined in the config file.
|
||||
* Verdaccio is able to listen multiple ports
|
||||
* eg:
|
||||
* listen:
|
||||
- localhost:5555
|
||||
- localhost:5557
|
||||
@return {Array}
|
||||
*/
|
||||
function get_listen_addresses() {
|
||||
// command line || config file || default
|
||||
let addresses;
|
||||
if (commander.listen) {
|
||||
addresses = [commander.listen];
|
||||
} else if (Array.isArray(config.listen)) {
|
||||
addresses = config.listen;
|
||||
} else if (config.listen) {
|
||||
addresses = [config.listen];
|
||||
} else {
|
||||
addresses = ['4873'];
|
||||
}
|
||||
addresses = addresses.map(function(addr) {
|
||||
let parsed_addr = Utils.parse_address(addr);
|
||||
|
||||
if (!parsed_addr) {
|
||||
logger.logger.warn({addr: addr},
|
||||
'invalid address - @{addr}, we expect a port (e.g. "4873"),'
|
||||
+ ' host:port (e.g. "localhost:4873") or full url'
|
||||
+ ' (e.g. "http://localhost:4873/")');
|
||||
}
|
||||
|
||||
return parsed_addr;
|
||||
}).filter(Boolean);
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger the server after configuration has been loaded.
|
||||
*/
|
||||
function afterConfigLoad() {
|
||||
if (!config.self_path) {
|
||||
config.self_path = Path.resolve(config_path);
|
||||
}
|
||||
if (!config.https) {
|
||||
config.https = {enable: false};
|
||||
}
|
||||
const app = server(config);
|
||||
get_listen_addresses().forEach(function(addr) {
|
||||
let webServer;
|
||||
if (addr.proto === 'https') { // https
|
||||
if (!config.https || !config.https.key || !config.https.cert || !config.https.ca) {
|
||||
let conf_path = function(file) {
|
||||
if (!file) return config_path;
|
||||
return Path.resolve(Path.dirname(config_path), file);
|
||||
};
|
||||
|
||||
logger.logger.fatal([
|
||||
'You need to specify "https.key", "https.cert" and "https.ca" to run https server',
|
||||
'',
|
||||
// commands are borrowed from node.js docs
|
||||
'To quickly create self-signed certificate, use:',
|
||||
' $ openssl genrsa -out ' + conf_path('verdaccio-key.pem') + ' 2048',
|
||||
' $ openssl req -new -sha256 -key ' + conf_path('verdaccio-key.pem') + ' -out ' + conf_path('verdaccio-csr.pem'),
|
||||
' $ openssl x509 -req -in ' + conf_path('verdaccio-csr.pem') +
|
||||
' -signkey ' + conf_path('verdaccio-key.pem') + ' -out ' + conf_path('verdaccio-cert.pem'),
|
||||
'',
|
||||
'And then add to config file (' + conf_path() + '):',
|
||||
' https:',
|
||||
' key: verdaccio-key.pem',
|
||||
' cert: verdaccio-cert.pem',
|
||||
' ca: verdaccio-cert.pem',
|
||||
].join('\n'));
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
try {
|
||||
webServer = https.createServer({
|
||||
secureProtocol: 'SSLv23_method', // disable insecure SSLv2 and SSLv3
|
||||
secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3,
|
||||
key: fs.readFileSync(config.https.key),
|
||||
cert: fs.readFileSync(config.https.cert),
|
||||
ca: fs.readFileSync(config.https.ca),
|
||||
}, app);
|
||||
} catch (err) { // catch errors related to certificate loading
|
||||
logger.logger.fatal({err: err}, 'cannot create server: @{err.message}');
|
||||
process.exit(2);
|
||||
}
|
||||
} else { // http
|
||||
webServer = http.createServer(app);
|
||||
}
|
||||
|
||||
webServer
|
||||
.listen(addr.port || addr.path, addr.host)
|
||||
.on('error', function(err) {
|
||||
logger.logger.fatal({err: err}, 'cannot create server: @{err.message}');
|
||||
process.exit(2);
|
||||
});
|
||||
|
||||
logger.logger.warn({
|
||||
addr: ( addr.path
|
||||
? URL.format({
|
||||
protocol: 'unix',
|
||||
pathname: addr.path,
|
||||
})
|
||||
: URL.format({
|
||||
protocol: addr.proto,
|
||||
hostname: addr.host,
|
||||
port: addr.port,
|
||||
pathname: '/',
|
||||
})
|
||||
),
|
||||
version: pkgName + '/' + pkgVersion,
|
||||
}, 'http address - @{addr} - @{version}');
|
||||
});
|
||||
|
||||
// undocumented stuff for tests
|
||||
if (_.isFunction(process.send)) {
|
||||
process.send({
|
||||
verdaccio_started: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
process.on('uncaughtException', function(err) {
|
||||
logger.logger.fatal( {
|
||||
err: err,
|
||||
},
|
||||
'uncaught exception, please report this\n@{err.stack}' );
|
||||
process.exit(255);
|
||||
});
|
||||
110
src/lib/config-path.js
Normal file
110
src/lib/config-path.js
Normal file
@@ -0,0 +1,110 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const Path = require('path');
|
||||
const logger = require('./logger');
|
||||
const CONFIG_FILE = 'config.yaml';
|
||||
const pkgJson = require('../../package.json');
|
||||
/**
|
||||
* Find and get the first config file that match.
|
||||
* @return {String} the config file path
|
||||
*/
|
||||
function find_config_file() {
|
||||
const paths = get_paths();
|
||||
|
||||
for (let i=0; i<paths.length; i++) {
|
||||
if (file_exists(paths[i].path)) return paths[i].path;
|
||||
}
|
||||
|
||||
create_config_file(paths[0]);
|
||||
return paths[0].path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default config file in your system.
|
||||
* @param {String} config_path
|
||||
*/
|
||||
function create_config_file(config_path) {
|
||||
require('mkdirp').sync(Path.dirname(config_path.path));
|
||||
logger.logger.info({file: config_path.path}, 'Creating default config file in @{file}');
|
||||
|
||||
let created_config = fs.readFileSync(require.resolve('../../conf/default.yaml'), 'utf8');
|
||||
|
||||
if (config_path.type === 'xdg') {
|
||||
// $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored,
|
||||
// If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used.
|
||||
let data_dir = process.env.XDG_DATA_HOME|| Path.join(process.env.HOME, '.local', 'share');
|
||||
if (folder_exists(data_dir)) {
|
||||
data_dir = Path.resolve(Path.join(data_dir, pkgJson.name, 'storage'));
|
||||
created_config = created_config.replace(/^storage: .\/storage$/m, `storage: ${data_dir}`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(config_path.path, created_config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of possible config file locations.
|
||||
* @return {Array}
|
||||
*/
|
||||
function get_paths() {
|
||||
let try_paths = [];
|
||||
let xdg_config = process.env.XDG_CONFIG_HOME
|
||||
|| process.env.HOME && Path.join(process.env.HOME, '.config');
|
||||
if (xdg_config && folder_exists(xdg_config)) {
|
||||
try_paths.push({
|
||||
path: Path.join(xdg_config, pkgJson.name, CONFIG_FILE),
|
||||
type: 'xdg',
|
||||
});
|
||||
}
|
||||
|
||||
if (process.platform === 'win32' && process.env.APPDATA && folder_exists(process.env.APPDATA)) {
|
||||
try_paths.push({
|
||||
path: Path.resolve(Path.join(process.env.APPDATA, pkgJson.name, CONFIG_FILE)),
|
||||
type: 'win',
|
||||
});
|
||||
}
|
||||
|
||||
try_paths.push({
|
||||
path: Path.resolve(Path.join('.', pkgJson.name, CONFIG_FILE)),
|
||||
type: 'def',
|
||||
});
|
||||
|
||||
// backward compatibility
|
||||
try_paths.push({
|
||||
path: Path.resolve(Path.join('.', CONFIG_FILE)),
|
||||
type: 'old',
|
||||
});
|
||||
|
||||
return try_paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the path already exist.
|
||||
* @param {String} path
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function folder_exists(path) {
|
||||
try {
|
||||
const stat = fs.statSync(path);
|
||||
return stat.isDirectory();
|
||||
} catch(_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the file already exist.
|
||||
* @param {String} path
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function file_exists(path) {
|
||||
try {
|
||||
const stat = fs.statSync(path);
|
||||
return stat.isFile();
|
||||
} catch(_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = find_config_file;
|
||||
225
src/lib/config.js
Normal file
225
src/lib/config.js
Normal file
@@ -0,0 +1,225 @@
|
||||
/* eslint prefer-rest-params: "off" */
|
||||
/* eslint prefer-spread: "off" */
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const _ = require('lodash');
|
||||
const Error = require('http-errors');
|
||||
const Crypto = require('crypto');
|
||||
const minimatch = require('minimatch');
|
||||
|
||||
const Utils = require('./utils');
|
||||
const pkginfo = require('pkginfo')(module); // eslint-disable-line no-unused-vars
|
||||
const pkgVersion = module.exports.version;
|
||||
const pkgName = module.exports.name;
|
||||
|
||||
/**
|
||||
* [[a, [b, c]], d] -> [a, b, c, d]
|
||||
* @param {*} array
|
||||
* @return {Array}
|
||||
*/
|
||||
function flatten(array) {
|
||||
let result = [];
|
||||
for (let i=0; i<array.length; i++) {
|
||||
if (Array.isArray(array[i])) {
|
||||
result.push.apply(result, flatten(array[i]));
|
||||
} else {
|
||||
result.push(array[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coordinates the application configuration
|
||||
*/
|
||||
class Config {
|
||||
|
||||
/**
|
||||
* @param {*} config config the content
|
||||
*/
|
||||
constructor(config) {
|
||||
const self = this;
|
||||
for (let i in config) {
|
||||
if (self[i] == null) {
|
||||
self[i] = config[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (!self.user_agent) {
|
||||
self.user_agent = `${pkgName}/${pkgVersion}`;
|
||||
}
|
||||
|
||||
// some weird shell scripts are valid yaml files parsed as string
|
||||
assert.equal(typeof(config), 'object', 'CONFIG: it doesn\'t look like a valid config file');
|
||||
|
||||
assert(self.storage, 'CONFIG: storage path not defined');
|
||||
|
||||
const users = {
|
||||
'all': true,
|
||||
'anonymous': true,
|
||||
'undefined': true,
|
||||
'owner': true,
|
||||
'none': true,
|
||||
};
|
||||
|
||||
const check_user_or_uplink = function(arg) {
|
||||
assert(arg !== 'all' && arg !== 'owner'
|
||||
&& arg !== 'anonymous' && arg !== 'undefined' && arg !== 'none', 'CONFIG: reserved user/uplink name: ' + arg);
|
||||
assert(!arg.match(/\s/), 'CONFIG: invalid user name: ' + arg);
|
||||
assert(users[arg] == null, 'CONFIG: duplicate user/uplink name: ' + arg);
|
||||
users[arg] = true;
|
||||
};
|
||||
|
||||
// sanity check for strategic config properties
|
||||
['users', 'uplinks', 'packages'].forEach(function(x) {
|
||||
if (self[x] == null) self[x] = {};
|
||||
assert(Utils.is_object(self[x]), `CONFIG: bad "${x}" value (object expected)`);
|
||||
});
|
||||
// sanity check for users
|
||||
for (let i in self.users) {
|
||||
if (Object.prototype.hasOwnProperty.call(self.users, i)) {
|
||||
check_user_or_uplink(i);
|
||||
}
|
||||
}
|
||||
// sanity check for uplinks
|
||||
for (let i in self.uplinks) {
|
||||
if (self.uplinks[i].cache == null) {
|
||||
self.uplinks[i].cache = true;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(self.uplinks, i)) {
|
||||
check_user_or_uplink(i);
|
||||
}
|
||||
}
|
||||
for (let i in self.users) {
|
||||
if (Object.prototype.hasOwnProperty.call(self.users, i)) {
|
||||
assert(self.users[i].password, 'CONFIG: no password for user: ' + i);
|
||||
assert(typeof(self.users[i].password) === 'string' &&
|
||||
self.users[i].password.match(/^[a-f0-9]{40}$/)
|
||||
, 'CONFIG: wrong password format for user: ' + i + ', sha1 expected');
|
||||
}
|
||||
}
|
||||
for (let i in self.uplinks) {
|
||||
if (Object.prototype.hasOwnProperty.call(self.uplinks, i)) {
|
||||
assert(self.uplinks[i].url, 'CONFIG: no url for uplink: ' + i);
|
||||
assert( typeof(self.uplinks[i].url) === 'string'
|
||||
, 'CONFIG: wrong url format for uplink: ' + i);
|
||||
self.uplinks[i].url = self.uplinks[i].url.replace(/\/$/, '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise user list.
|
||||
* @return {Array}
|
||||
*/
|
||||
function normalize_userlist() {
|
||||
let result = [];
|
||||
|
||||
for (let i=0; i<arguments.length; i++) {
|
||||
if (arguments[i] == null) continue;
|
||||
|
||||
// if it's a string, split it to array
|
||||
if (typeof(arguments[i]) === 'string') {
|
||||
result.push(arguments[i].split(/\s+/));
|
||||
} else if (Array.isArray(arguments[i])) {
|
||||
result.push(arguments[i]);
|
||||
} else {
|
||||
throw Error('CONFIG: bad package acl (array or string expected): ' + JSON.stringify(arguments[i]));
|
||||
}
|
||||
}
|
||||
return flatten(result);
|
||||
}
|
||||
|
||||
// add a default rule for all packages to make writing plugins easier
|
||||
if (self.packages['**'] == null) {
|
||||
self.packages['**'] = {};
|
||||
}
|
||||
|
||||
for (let i in self.packages) {
|
||||
if (Object.prototype.hasOwnProperty.call(self.packages, i)) {
|
||||
assert(
|
||||
typeof(self.packages[i]) === 'object' &&
|
||||
!Array.isArray(self.packages[i])
|
||||
, 'CONFIG: bad "'+i+'" package description (object expected)');
|
||||
|
||||
self.packages[i].access = normalize_userlist(
|
||||
self.packages[i].allow_access,
|
||||
self.packages[i].access
|
||||
);
|
||||
delete self.packages[i].allow_access;
|
||||
|
||||
self.packages[i].publish = normalize_userlist(
|
||||
self.packages[i].allow_publish,
|
||||
self.packages[i].publish
|
||||
);
|
||||
delete self.packages[i].allow_publish;
|
||||
|
||||
self.packages[i].proxy = normalize_userlist(
|
||||
self.packages[i].proxy_access,
|
||||
self.packages[i].proxy
|
||||
);
|
||||
delete self.packages[i].proxy_access;
|
||||
}
|
||||
}
|
||||
|
||||
// loading these from ENV if aren't in config
|
||||
['http_proxy', 'https_proxy', 'no_proxy'].forEach((function(v) {
|
||||
if (!(v in self)) {
|
||||
self[v] = process.env[v] || process.env[v.toUpperCase()];
|
||||
}
|
||||
}));
|
||||
|
||||
// unique identifier of self server (or a cluster), used to avoid loops
|
||||
if (!self.server_id) {
|
||||
self.server_id = Crypto.pseudoRandomBytes(6).toString('hex');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an uplink can proxy
|
||||
* @param {String} pkg package anem
|
||||
* @param {*} upLink
|
||||
* @return {Boolean}
|
||||
*/
|
||||
hasProxyTo(pkg, upLink) {
|
||||
return (this.getMatchedPackagesSpec(pkg).proxy || []).reduce(function(prev, curr) {
|
||||
if (upLink === curr) {
|
||||
return true;
|
||||
}
|
||||
return prev;
|
||||
}, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for package spec
|
||||
* @param {String} pkg package name
|
||||
* @return {Object}
|
||||
*/
|
||||
getMatchedPackagesSpec(pkg) {
|
||||
for (let i in this.packages) {
|
||||
if (minimatch.makeRe(i).exec(pkg)) {
|
||||
return this.packages[i];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Store or create whether recieve a secret key
|
||||
* @param {String} secret
|
||||
* @return {String}
|
||||
*/
|
||||
checkSecretKey(secret) {
|
||||
if (_.isNil(secret) === false) {
|
||||
this.secret = secret;
|
||||
return secret;
|
||||
}
|
||||
// it generates a secret key
|
||||
// FUTURE: this might be an external secret key, perhaps whitin config file?
|
||||
this.secret = Crypto.pseudoRandomBytes(32).toString('hex');
|
||||
return this.secret;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Config;
|
||||
196
src/lib/logger.js
Normal file
196
src/lib/logger.js
Normal file
@@ -0,0 +1,196 @@
|
||||
'use strict';
|
||||
|
||||
const Logger = require('bunyan');
|
||||
const Error = require('http-errors');
|
||||
const Stream = require('stream');
|
||||
const chalk = require('chalk');
|
||||
const Utils = require('./utils');
|
||||
const pkgJSON = require('../../package.json');
|
||||
|
||||
/**
|
||||
* Match the level based on buyan severity scale
|
||||
* @param {*} x severity level
|
||||
* @return {String} security level
|
||||
*/
|
||||
function getlvl(x) {
|
||||
switch(true) {
|
||||
case x < 15 : return 'trace';
|
||||
case x < 25 : return 'debug';
|
||||
case x < 35 : return 'info';
|
||||
case x == 35 : return 'http';
|
||||
case x < 45 : return 'warn';
|
||||
case x < 55 : return 'error';
|
||||
default : return 'fatal';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the Buyan logger
|
||||
* @param {*} logs list of log configuration
|
||||
*/
|
||||
function setup(logs) {
|
||||
let streams = [];
|
||||
if (logs == null) {
|
||||
logs = [{type: 'stdout', format: 'pretty', level: 'http'}];
|
||||
}
|
||||
|
||||
logs.forEach(function(target) {
|
||||
// create a stream for each log configuration
|
||||
const stream = new Stream();
|
||||
stream.writable = true;
|
||||
|
||||
if (target.type === 'stdout' || target.type === 'stderr') {
|
||||
// destination stream
|
||||
const dest = target.type === 'stdout' ? process.stdout : process.stderr;
|
||||
|
||||
if (target.format === 'pretty') {
|
||||
// making fake stream for prettypritting
|
||||
stream.write = function(obj) {
|
||||
dest.write(print(obj.level, obj.msg, obj, dest.isTTY) + '\n');
|
||||
};
|
||||
} else if (target.format === 'pretty-timestamped') {
|
||||
// making fake stream for prettypritting
|
||||
stream.write = function(obj) {
|
||||
dest.write(obj.time.toISOString() + print(obj.level, obj.msg, obj, dest.isTTY) + '\n');
|
||||
};
|
||||
} else {
|
||||
stream.write = function(obj) {
|
||||
dest.write(JSON.stringify(obj, Logger.safeCycles()) + '\n');
|
||||
};
|
||||
}
|
||||
} else if (target.type === 'file') {
|
||||
const dest = require('fs').createWriteStream(target.path, {flags: 'a', encoding: 'utf8'});
|
||||
dest.on('error', function(err) {
|
||||
Logger.emit('error', err);
|
||||
});
|
||||
stream.write = function(obj) {
|
||||
if (target.format === 'pretty') {
|
||||
dest.write(print(obj.level, obj.msg, obj, false) + '\n');
|
||||
} else {
|
||||
dest.write(JSON.stringify(obj, Logger.safeCycles()) + '\n');
|
||||
}
|
||||
};
|
||||
} else {
|
||||
throw Error('wrong target type for a log');
|
||||
}
|
||||
|
||||
if (target.level === 'http') target.level = 35;
|
||||
streams.push({
|
||||
type: 'raw',
|
||||
level: target.level || 35,
|
||||
stream: stream,
|
||||
});
|
||||
});
|
||||
|
||||
// buyan default configuration
|
||||
const logger = new Logger({
|
||||
name: pkgJSON.name,
|
||||
streams: streams,
|
||||
serializers: {
|
||||
err: Logger.stdSerializers.err,
|
||||
req: Logger.stdSerializers.req,
|
||||
res: Logger.stdSerializers.res,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports.logger = logger;
|
||||
}
|
||||
|
||||
// adopted from socket.io
|
||||
// this part was converted to coffee-script and back again over the years,
|
||||
// so it might look weird
|
||||
|
||||
// level to color
|
||||
const levels = {
|
||||
fatal: chalk.red,
|
||||
error: chalk.red,
|
||||
warn: chalk.yellow,
|
||||
http: chalk.magenta,
|
||||
info: chalk.cyan,
|
||||
debug: chalk.green,
|
||||
trace: chalk.white,
|
||||
};
|
||||
|
||||
let max = 0;
|
||||
for (let l in levels) {
|
||||
if (Object.prototype.hasOwnProperty.call(levels, l)) {
|
||||
max = Math.max(max, l.length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply whitespaces based on the length
|
||||
* @param {*} str the log message
|
||||
* @return {String}
|
||||
*/
|
||||
function pad(str) {
|
||||
if (str.length < max) {
|
||||
return str + ' '.repeat(max - str.length);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply colors to a string based on level parameters.
|
||||
* @param {*} type
|
||||
* @param {*} msg
|
||||
* @param {*} obj
|
||||
* @param {*} colors
|
||||
* @return {String}
|
||||
*/
|
||||
function print(type, msg, obj, colors) {
|
||||
if (typeof type === 'number') {
|
||||
type = getlvl(type);
|
||||
}
|
||||
let finalmsg = msg.replace(/@{(!?[$A-Za-z_][$0-9A-Za-z\._]*)}/g, function(_, name) {
|
||||
let str = obj;
|
||||
let is_error;
|
||||
if (name[0] === '!') {
|
||||
name = name.substr(1);
|
||||
is_error = true;
|
||||
}
|
||||
|
||||
let _ref = name.split('.');
|
||||
for (let _i = 0; _i < _ref.length; _i++) {
|
||||
let id = _ref[_i];
|
||||
if (Utils.is_object(str) || Array.isArray(str)) {
|
||||
str = str[id];
|
||||
} else {
|
||||
str = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof(str) === 'string') {
|
||||
if (!colors || str.includes('\n')) {
|
||||
return str;
|
||||
} else if (is_error) {
|
||||
return chalk.red(str);
|
||||
} else {
|
||||
return chalk.green(str);
|
||||
}
|
||||
} else {
|
||||
return require('util').inspect(str, null, null, colors);
|
||||
}
|
||||
});
|
||||
|
||||
const subsystems = [{
|
||||
in: chalk.green('<--'),
|
||||
out: chalk.yellow('-->'),
|
||||
fs: chalk.black('-=-'),
|
||||
default: chalk.blue('---'),
|
||||
}, {
|
||||
in: '<--',
|
||||
out: '-->',
|
||||
fs: '-=-',
|
||||
default: '---',
|
||||
}];
|
||||
|
||||
const sub = subsystems[colors ? 0 : 1][obj.sub] || subsystems[+!colors].default;
|
||||
if (colors) {
|
||||
return ` ${levels[type]((pad(type)))}${chalk.white(`${sub} ${finalmsg}`)}`;
|
||||
} else {
|
||||
return ` ${(pad(type))}${sub} ${finalmsg}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.setup = setup;
|
||||
81
src/lib/notify.js
Normal file
81
src/lib/notify.js
Normal file
@@ -0,0 +1,81 @@
|
||||
'use strict';
|
||||
|
||||
const Handlebars = require('handlebars');
|
||||
const request = require('request');
|
||||
const _ = require('lodash');
|
||||
const logger = require('./logger');
|
||||
|
||||
const handleNotify = function(metadata, notifyEntry) {
|
||||
let regex;
|
||||
if (metadata.name && notifyEntry.packagePattern) {
|
||||
// FUTURE: comment out due https://github.com/verdaccio/verdaccio/pull/108#issuecomment-312421052
|
||||
// regex = new RegExp(notifyEntry.packagePattern, notifyEntry.packagePatternFlags || '');
|
||||
regex = new RegExp(notifyEntry.packagePattern);
|
||||
if (!regex.test(metadata.name)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const template = Handlebars.compile(notifyEntry.content);
|
||||
const content = template( metadata );
|
||||
|
||||
const options = {
|
||||
body: content,
|
||||
};
|
||||
|
||||
// provides fallback support, it's accept an Object {} and Array of {}
|
||||
if (notifyEntry.headers && _.isArray(notifyEntry.headers)) {
|
||||
const header = {};
|
||||
notifyEntry.headers.map(function(item) {
|
||||
if (Object.is(item, item)) {
|
||||
for (const key in item) {
|
||||
if (item.hasOwnProperty(key)) {
|
||||
header[key] = item[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
options.headers = header;
|
||||
} else if (Object.is(notifyEntry.headers, notifyEntry.headers)) {
|
||||
options.headers = notifyEntry.headers;
|
||||
}
|
||||
|
||||
options.method = notifyEntry.method;
|
||||
|
||||
if ( notifyEntry.endpoint ) {
|
||||
options.url = notifyEntry.endpoint;
|
||||
}
|
||||
|
||||
return new Promise(( resolve, reject) => {
|
||||
request(options, function(err, response, body) {
|
||||
if (err || response.statusCode >= 400) {
|
||||
const errorMessage = _.isNil(err) ? response.statusMessage : err;
|
||||
logger.logger.error({err: errorMessage}, ' notify error: @{err.message}' );
|
||||
reject(errorMessage);
|
||||
} else {
|
||||
logger.logger.info({content: content}, 'A notification has been shipped: @{content}');
|
||||
if (body) {
|
||||
logger.logger.debug({body: body}, ' body: @{body}' );
|
||||
}
|
||||
resolve(_.isNil(body) === false ? body : null);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const notify = function(metadata, config) {
|
||||
if (config.notify) {
|
||||
if (config.notify.content) {
|
||||
return handleNotify(metadata, config.notify);
|
||||
} else {
|
||||
// multiple notifications endpoints PR #108
|
||||
for (const key in config.notify) {
|
||||
if (config.notify.hasOwnProperty(key)) {
|
||||
return handleNotify(metadata, config.notify[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.notify = notify;
|
||||
77
src/lib/plugin-loader.js
Normal file
77
src/lib/plugin-loader.js
Normal file
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
|
||||
const Path = require('path');
|
||||
|
||||
/**
|
||||
* Requires a module.
|
||||
* @param {*} path the module's path
|
||||
* @return {Object}
|
||||
*/
|
||||
function try_load(path) {
|
||||
try {
|
||||
return require(path);
|
||||
} catch(err) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a plugin following the rules
|
||||
* - First try to load from the internal directory plugins (which will disappear soon or later).
|
||||
* - A seccond attempt from node_modules, in case to have multiple match as for instance verdaccio-ldap
|
||||
* and sinopia-ldap. All verdaccio prefix will have preferences.
|
||||
* @param {*} config a reference of the configuration settings
|
||||
* @param {*} plugin_configs
|
||||
* @param {*} params a set of params to initialise the plugin
|
||||
* @param {*} sanity_check callback that check the shape that should fulfill the plugin
|
||||
* @return {Array} list of plugins
|
||||
*/
|
||||
function load_plugins(config, plugin_configs, params, sanity_check) {
|
||||
let plugins = Object.keys(plugin_configs || {}).map(function(p) {
|
||||
let plugin;
|
||||
|
||||
// try local plugins first
|
||||
plugin = try_load(Path.resolve(__dirname + '/..//plugins', p));
|
||||
|
||||
// npm package
|
||||
if (plugin === null && p.match(/^[^\.\/]/)) {
|
||||
plugin = try_load(`verdaccio-${p}`);
|
||||
// compatibility for old sinopia plugins
|
||||
if (!plugin) {
|
||||
plugin = try_load(`sinopia-${p}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin === null) {
|
||||
plugin = try_load(p);
|
||||
}
|
||||
|
||||
// relative to config path
|
||||
if (plugin === null && p.match(/^\.\.?($|\/)/)) {
|
||||
plugin = try_load(Path.resolve(Path.dirname(config.self_path), p));
|
||||
}
|
||||
|
||||
if (plugin === null) {
|
||||
throw Error('"' + p + '" plugin not found\ntry "npm install verdaccio-' + p + '"');
|
||||
}
|
||||
|
||||
if (typeof(plugin) !== 'function') {
|
||||
throw Error('"' + p + '" doesn\'t look like a valid plugin');
|
||||
}
|
||||
|
||||
plugin = plugin(plugin_configs[p], params);
|
||||
|
||||
if (plugin === null || !sanity_check(plugin)) {
|
||||
throw Error('"' + p + '" doesn\'t look like a valid plugin');
|
||||
}
|
||||
|
||||
return plugin;
|
||||
});
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
exports.load_plugins = load_plugins;
|
||||
83
src/lib/search.js
Normal file
83
src/lib/search.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/* eslint no-invalid-this: "off" */
|
||||
|
||||
'use strict';
|
||||
|
||||
const lunr = require('lunr');
|
||||
|
||||
/**
|
||||
* Handle the search Indexer.
|
||||
*/
|
||||
class Search {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
constructor() {
|
||||
this.index = lunr(function() {
|
||||
this.field('name', {boost: 10});
|
||||
this.field('description', {boost: 4});
|
||||
this.field('author', {boost: 6});
|
||||
this.field('readme');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a query to the indexer.
|
||||
* If the keyword is a * it returns all local elements
|
||||
* otherwise performs a search
|
||||
* @param {*} q the keyword
|
||||
* @return {Array} list of results.
|
||||
*/
|
||||
query(q) {
|
||||
return q === '*'
|
||||
? this.storage.localStorage.localList.get().map( function( pkg ) {
|
||||
return {ref: pkg, score: 1};
|
||||
}) : this.index.search(q);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new element to index
|
||||
* @param {*} pkg the package
|
||||
*/
|
||||
add(pkg) {
|
||||
this.index.add({
|
||||
id: pkg.name,
|
||||
name: pkg.name,
|
||||
description: pkg.description,
|
||||
author: pkg._npmUser ? pkg._npmUser.name : '???',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an element from the index.
|
||||
* @param {*} name the id element
|
||||
*/
|
||||
remove(name) {
|
||||
this.index.remove({id: name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a reindex.
|
||||
*/
|
||||
reindex() {
|
||||
let self = this;
|
||||
this.storage.get_local(function(err, packages) {
|
||||
if (err) throw err; // that function shouldn't produce any
|
||||
let i = packages.length;
|
||||
while (i--) {
|
||||
self.add(packages[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the {Storage}
|
||||
* @param {*} storage An storage reference.
|
||||
*/
|
||||
configureStorage(storage) {
|
||||
this.storage = storage;
|
||||
this.reindex();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Search();
|
||||
77
src/lib/status-cats.js
Normal file
77
src/lib/status-cats.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/* eslint prefer-rest-params: "off" */
|
||||
|
||||
'use strict';
|
||||
|
||||
// see https://secure.flickr.com/photos/girliemac/sets/72157628409467125
|
||||
|
||||
const images = {
|
||||
100: 'aVvDhR', // '6512768893', // 100 - Continue
|
||||
101: 'aXXExP', // '6540479029', // 101 - Switching Protocols
|
||||
200: 'aVuVsF', // '6512628175', // 200 - OK
|
||||
201: 'aXWm1Z', // '6540221577', // 201 - Created
|
||||
202: 'aXXEyF', // '6540479079', // 202 - Accepted
|
||||
204: 'aYyJ7B', // '6547319943', // 204 - No Content
|
||||
206: 'aVEnUP', // '6514473163', // 206 - Partial Content
|
||||
207: 'aVEnRD', // '6514472979', // 207 - Multi-Status
|
||||
300: 'aW7mac', // '6519540181', // 300 - Multiple Choices
|
||||
301: 'aW7mb4', // '6519540231', // 301 - Moved Permanently
|
||||
302: 'aV6jKp', // '6508023829', // 302 - Found
|
||||
303: 'aVxtaK', // '6513125065', // 303 - See Other
|
||||
304: 'aXY3dH', // '6540551929', // 304 - Not Modified
|
||||
305: 'aXX5LK', // '6540365403', // 305 - Use Proxy
|
||||
307: 'aVwQnk', // '6513001269', // 307 - Temporary Redirect
|
||||
400: 'aXYDeT', // '6540669737', // 400 - Bad Request
|
||||
401: 'aV6jwe', // '6508023065', // 401 - Unauthorized
|
||||
402: 'aVwQoe', // '6513001321', // 402 - Payment Required
|
||||
403: 'aV6jFK', // '6508023617', // 403 - Forbidden
|
||||
404: 'aV6juR', // '6508022985', // 404 - Not Found
|
||||
405: 'aV6jE8', // '6508023523', // 405 - Method Not Allowed
|
||||
406: 'aV6jxa', // '6508023119', // 406 - Not Acceptable
|
||||
408: 'aV6jyc', // '6508023179', // 408 - Request Timeout
|
||||
409: 'aV6jzz', // '6508023259', // 409 - Conflict
|
||||
410: 'aVES2H', // '6514567755', // 410 - Gone
|
||||
411: 'aXYVpT', // '6540724141', // 411 - Length Required
|
||||
413: 'aV6jHZ', // '6508023747', // 413 - Request Entity Too Large
|
||||
414: 'aV6jBa', // '6508023351', // 414 - Request-URI Too Long
|
||||
416: 'aVxQvr', // '6513196851', // 416 - Requested Range Not Satisfiable
|
||||
417: 'aV6jGP', // '6508023679', // 417 - Expectation Failed
|
||||
418: 'aV6J7c', // '6508102407', // 418 - I'm a teapot
|
||||
422: 'aVEnTt', // '6514473085', // 422 - Unprocessable Entity
|
||||
423: 'aVEyVZ', // '6514510235', // 423 - Locked
|
||||
424: 'aVEWZ6', // '6514584423', // 424 - Failed Dependency
|
||||
425: 'aXYdzH', // '6540586787', // 425 - Unordered Collection
|
||||
426: 'aVdo4M', // '6509400771', // 426 - Upgrade Required
|
||||
429: 'aVdo8F', // '6509400997', // 429 - Too Many Requests
|
||||
431: 'aVdo3n', // '6509400689', // 431 - Request Header Fields Too Large
|
||||
444: 'aVdo1P', // '6509400599', // 444 - No Response
|
||||
450: 'aVxtbK', // '6513125123', // 450 - Blocked by Windows Parental Controls
|
||||
451: 'eTiGQd', // '9113233540', // 451 - Unavailable for Legal Reasons
|
||||
500: 'aVdo6e', // '6509400855', // 500 - Internal Server Error
|
||||
502: 'aV6jCv', // '6508023429', // 502 - Bad Gateway
|
||||
503: 'aXYvop', // '6540643319', // 503 - Service Unavailable
|
||||
506: 'aXYvnH', // '6540643279', // 506 - Variant Also Negotiates
|
||||
507: 'aVdnZa', // '6509400503', // 507 - Insufficient Storage
|
||||
508: 'aVdnYa', // '6509400445', // 508 - Loop Detected
|
||||
509: 'aXXg1V', // '6540399865', // 509 - Bandwidth Limit Exceeded
|
||||
599: 'aVdo7v', // '6509400929', // 599 - Network connect timeout error
|
||||
};
|
||||
|
||||
module.exports.get_image = function(status) {
|
||||
if (status in images) {
|
||||
return 'http://flic.kr/p/' + images[status];
|
||||
// return 'https://secure.flickr.com/photos/girliemac/'+images[status]+'/in/set-72157628409467125/lightbox/'
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.middleware = function(req, res, next) {
|
||||
let _writeHead = res.writeHead;
|
||||
res.writeHead = function(status) {
|
||||
if (status in images) {
|
||||
res.setHeader('X-Status-Cat', module.exports.get_image(status));
|
||||
}
|
||||
_writeHead.apply(res, arguments);
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
671
src/lib/storage.js
Normal file
671
src/lib/storage.js
Normal file
@@ -0,0 +1,671 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const assert = require('assert');
|
||||
const async = require('async');
|
||||
const Error = require('http-errors');
|
||||
const semver = require('semver');
|
||||
const Stream = require('stream');
|
||||
|
||||
const Search = require('./search');
|
||||
const LocalStorage = require('./storage/local/local-storage');
|
||||
const Logger = require('./logger');
|
||||
const MyStreams = require('@verdaccio/streams');
|
||||
const Proxy = require('./storage/up-storage');
|
||||
const Utils = require('./utils');
|
||||
|
||||
const WHITELIST = ['_rev', 'name', 'versions', 'dist-tags', 'readme', 'time'];
|
||||
const getDefaultMetadata = (name) => {
|
||||
return {
|
||||
'name': name,
|
||||
'versions': {},
|
||||
'dist-tags': {},
|
||||
'_uplinks': {},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements Storage interface
|
||||
* (same for storage.js, local-storage.js, up-storage.js).
|
||||
*/
|
||||
class Storage {
|
||||
|
||||
/**
|
||||
* @param {*} config
|
||||
*/
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this._setupUpLinks(this.config);
|
||||
this.localStorage = new LocalStorage(config, Logger.logger, Utils);
|
||||
this.localStorage.localList.data.secret = this.config.checkSecretKey(this.localStorage.localList.data.secret);
|
||||
this.localStorage.localList.sync();
|
||||
// an instance for local storage
|
||||
this.logger = Logger.logger.child();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a {name} package to a system
|
||||
Function checks if package with the same name is available from uplinks.
|
||||
If it isn't, we create package locally
|
||||
Used storages: local (write) && uplinks
|
||||
* @param {*} name
|
||||
* @param {*} metadata
|
||||
* @param {*} callback
|
||||
*/
|
||||
addPackage(name, metadata, callback) {
|
||||
const self = this;
|
||||
|
||||
/**
|
||||
* Check whether a package it is already a local package
|
||||
* @return {Promise}
|
||||
*/
|
||||
const checkPackageLocal = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.localStorage.getPackageMetadata(name, {}, (err, results) => {
|
||||
if (!_.isNil(err) && err.status !== 404) {
|
||||
return reject(err);
|
||||
}
|
||||
if (results) {
|
||||
return reject(Error[409]('this package is already present'));
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether a package exist in any of the uplinks.
|
||||
* @return {Promise}
|
||||
*/
|
||||
const checkPackageRemote = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
self._syncUplinksMetadata(name, null, {}, (err, results, err_results) => {
|
||||
// something weird
|
||||
if (err && err.status !== 404) {
|
||||
return reject(err);
|
||||
}
|
||||
// checking package
|
||||
if (results) {
|
||||
return reject(Error[409]('this package is already present'));
|
||||
}
|
||||
for (let i = 0; i < err_results.length; i++) {
|
||||
// checking error
|
||||
// if uplink fails with a status other than 404, we report failure
|
||||
if (_.isNil(err_results[i][0]) === false) {
|
||||
if (err_results[i][0].status !== 404) {
|
||||
if (_.isNil(this.config.publish) === false &&
|
||||
_.isBoolean(this.config.publish.allow_offline) &&
|
||||
this.config.publish.allow_offline) {
|
||||
return resolve();
|
||||
}
|
||||
return reject(Error[503]('one of the uplinks is down, refuse to publish'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a package to the local database
|
||||
* @return {Promise}
|
||||
*/
|
||||
const publishPackage = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
self.localStorage.addPackage(name, metadata, (err, latest) => {
|
||||
if (!_.isNull(err)) {
|
||||
return reject(err);
|
||||
} else if (!_.isUndefined(latest)) {
|
||||
Search.add(latest);
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// NOTE:
|
||||
// - when we checking package for existance, we ask ALL uplinks
|
||||
// - when we publishing package, we only publish it to some of them
|
||||
// so all requests are necessary
|
||||
checkPackageLocal()
|
||||
.then(() => {
|
||||
return checkPackageRemote().then(() => {
|
||||
return publishPackage().then(() => {
|
||||
callback();
|
||||
}, (err) => callback(err));
|
||||
}, (err) => callback(err));
|
||||
}, (err) => callback(err));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new version of package {name} to a system
|
||||
Used storages: local (write)
|
||||
* @param {*} name
|
||||
* @param {*} version
|
||||
* @param {*} metadata
|
||||
* @param {*} tag
|
||||
* @param {*} callback
|
||||
*/
|
||||
add_version(name, version, metadata, tag, callback) {
|
||||
this.localStorage.addVersion(name, version, metadata, tag, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tags a package version with a provided tag
|
||||
Used storages: local (write)
|
||||
* @param {*} name
|
||||
* @param {*} tag_hash
|
||||
* @param {*} callback
|
||||
*/
|
||||
merge_tags(name, tag_hash, callback) {
|
||||
this.localStorage.mergeTags(name, tag_hash, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tags a package version with a provided tag
|
||||
Used storages: local (write)
|
||||
* @param {*} name
|
||||
* @param {*} tag_hash
|
||||
* @param {*} callback
|
||||
*/
|
||||
replace_tags(name, tag_hash, callback) {
|
||||
this.localStorage.replaceTags(name, tag_hash, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change an existing package (i.e. unpublish one version)
|
||||
Function changes a package info from local storage and all uplinks with write access./
|
||||
Used storages: local (write)
|
||||
* @param {*} name
|
||||
* @param {*} metadata
|
||||
* @param {*} revision
|
||||
* @param {*} callback
|
||||
*/
|
||||
change_package(name, metadata, revision, callback) {
|
||||
this.localStorage.changePackage(name, metadata, revision, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a package from a system
|
||||
Function removes a package from local storage
|
||||
Used storages: local (write)
|
||||
* @param {*} name
|
||||
* @param {*} callback
|
||||
*/
|
||||
remove_package(name, callback) {
|
||||
this.localStorage.removePackage(name, callback);
|
||||
// update the indexer
|
||||
Search.remove(name);
|
||||
}
|
||||
|
||||
/**
|
||||
Remove a tarball from a system
|
||||
Function removes a tarball from local storage.
|
||||
Tarball in question should not be linked to in any existing
|
||||
versions, i.e. package version should be unpublished first.
|
||||
Used storage: local (write)
|
||||
* @param {*} name
|
||||
* @param {*} filename
|
||||
* @param {*} revision
|
||||
* @param {*} callback
|
||||
*/
|
||||
remove_tarball(name, filename, revision, callback) {
|
||||
this.localStorage.removeTarball(name, filename, revision, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a tarball for {name} package
|
||||
Function is syncronous and returns a WritableStream
|
||||
Used storages: local (write)
|
||||
* @param {*} name
|
||||
* @param {*} filename
|
||||
* @return {Stream}
|
||||
*/
|
||||
add_tarball(name, filename) {
|
||||
return this.localStorage.addTarball(name, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
Get a tarball from a storage for {name} package
|
||||
Function is syncronous and returns a ReadableStream
|
||||
Function tries to read tarball locally, if it fails then it reads package
|
||||
information in order to figure out where we can get this tarball from
|
||||
Used storages: local || uplink (just one)
|
||||
* @param {*} name
|
||||
* @param {*} filename
|
||||
* @return {Stream}
|
||||
*/
|
||||
get_tarball(name, filename) {
|
||||
let readStream = new MyStreams.ReadTarball();
|
||||
readStream.abort = function() {};
|
||||
|
||||
let self = this;
|
||||
|
||||
// if someone requesting tarball, it means that we should already have some
|
||||
// information about it, so fetching package info is unnecessary
|
||||
|
||||
// trying local first
|
||||
let rstream = self.localStorage.getTarball(name, filename);
|
||||
let is_open = false;
|
||||
rstream.on('error', function(err) {
|
||||
if (is_open || err.status !== 404) {
|
||||
return readStream.emit('error', err);
|
||||
}
|
||||
|
||||
// local reported 404
|
||||
let err404 = err;
|
||||
rstream.abort();
|
||||
rstream = null; // gc
|
||||
self.localStorage.getPackageMetadata(name, (err, info) => {
|
||||
if (_.isNil(err) && info._distfiles && _.isNil(info._distfiles[filename]) === false) {
|
||||
// information about this file exists locally
|
||||
serveFile(info._distfiles[filename]);
|
||||
} else {
|
||||
// we know nothing about this file, trying to get information elsewhere
|
||||
self._syncUplinksMetadata(name, info, {}, (err, info) => {
|
||||
if (_.isNil(err) === false) {
|
||||
return readStream.emit('error', err);
|
||||
}
|
||||
if (_.isNil(info._distfiles) || _.isNil(info._distfiles[filename])) {
|
||||
return readStream.emit('error', err404);
|
||||
}
|
||||
serveFile(info._distfiles[filename]);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
rstream.on('content-length', function(v) {
|
||||
readStream.emit('content-length', v);
|
||||
});
|
||||
rstream.on('open', function() {
|
||||
is_open = true;
|
||||
rstream.pipe(readStream);
|
||||
});
|
||||
return readStream;
|
||||
|
||||
/**
|
||||
* Fetch and cache local/remote packages.
|
||||
* @param {Object} file define the package shape
|
||||
*/
|
||||
function serveFile(file) {
|
||||
let uplink = null;
|
||||
for (let p in self.uplinks) {
|
||||
if (self.uplinks[p].isUplinkValid(file.url)) {
|
||||
uplink = self.uplinks[p];
|
||||
}
|
||||
}
|
||||
if (uplink == null) {
|
||||
uplink = new Proxy({
|
||||
url: file.url,
|
||||
cache: true,
|
||||
_autogenerated: true,
|
||||
}, self.config);
|
||||
}
|
||||
let savestream = null;
|
||||
if (uplink.config.cache) {
|
||||
savestream = self.localStorage.addTarball(name, filename);
|
||||
}
|
||||
let on_open = function() {
|
||||
// prevent it from being called twice
|
||||
on_open = function() {};
|
||||
let rstream2 = uplink.fetchTarball(file.url);
|
||||
rstream2.on('error', function(err) {
|
||||
if (savestream) {
|
||||
savestream.abort();
|
||||
}
|
||||
savestream = null;
|
||||
readStream.emit('error', err);
|
||||
});
|
||||
rstream2.on('end', function() {
|
||||
if (savestream) {
|
||||
savestream.done();
|
||||
}
|
||||
});
|
||||
|
||||
rstream2.on('content-length', function(v) {
|
||||
readStream.emit('content-length', v);
|
||||
if (savestream) {
|
||||
savestream.emit('content-length', v);
|
||||
}
|
||||
});
|
||||
rstream2.pipe(readStream);
|
||||
if (savestream) {
|
||||
rstream2.pipe(savestream);
|
||||
}
|
||||
};
|
||||
|
||||
if (savestream) {
|
||||
savestream.on('open', function() {
|
||||
on_open();
|
||||
});
|
||||
|
||||
savestream.on('error', function(err) {
|
||||
self.logger.warn( {err: err}
|
||||
, 'error saving file: @{err.message}\n@{err.stack}' );
|
||||
if (savestream) {
|
||||
savestream.abort();
|
||||
}
|
||||
savestream = null;
|
||||
on_open();
|
||||
});
|
||||
} else {
|
||||
on_open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Retrieve a package metadata for {name} package
|
||||
Function invokes localStorage.getPackage and uplink.get_package for every
|
||||
uplink with proxy_access rights against {name} and combines results
|
||||
into one json object
|
||||
Used storages: local && uplink (proxy_access)
|
||||
* @param {*} name
|
||||
* @param {*} options
|
||||
* @param {*} callback
|
||||
*/
|
||||
get_package(name, options, callback) {
|
||||
if (_.isFunction(options)) {
|
||||
callback = options, options = {};
|
||||
}
|
||||
|
||||
this.localStorage.getPackageMetadata(name, options, (err, data) => {
|
||||
if (err && (!err.status || err.status >= 500)) {
|
||||
// report internal errors right away
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
this._syncUplinksMetadata(name, data, options, function(err, result, uplink_errors) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
for (let i in result) {
|
||||
if (WHITELIST.indexOf(i) === -1) {
|
||||
delete result[i];
|
||||
}
|
||||
}
|
||||
|
||||
Utils.normalize_dist_tags(result);
|
||||
|
||||
// npm can throw if this field doesn't exist
|
||||
result._attachments = {};
|
||||
|
||||
callback(null, result, uplink_errors);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
Retrieve remote and local packages more recent than {startkey}
|
||||
Function streams all packages from all uplinks first, and then
|
||||
local packages.
|
||||
Note that local packages could override registry ones just because
|
||||
they appear in JSON last. That's a trade-off we make to avoid
|
||||
memory issues.
|
||||
Used storages: local && uplink (proxy_access)
|
||||
* @param {*} startkey
|
||||
* @param {*} options
|
||||
* @return {Stream}
|
||||
*/
|
||||
search(startkey, options) {
|
||||
let self = this;
|
||||
// stream to write a tarball
|
||||
let stream = new Stream.PassThrough({objectMode: true});
|
||||
|
||||
async.eachSeries(Object.keys(this.uplinks), function(up_name, cb) {
|
||||
// shortcut: if `local=1` is supplied, don't call uplinks
|
||||
if (options.req.query.local !== undefined) {
|
||||
return cb();
|
||||
}
|
||||
// search by keyword for each uplink
|
||||
let lstream = self.uplinks[up_name].search(options);
|
||||
// join streams
|
||||
lstream.pipe(stream, {end: false});
|
||||
lstream.on('error', function(err) {
|
||||
self.logger.error({err: err}, 'uplink error: @{err.message}');
|
||||
cb(), cb = function() {};
|
||||
});
|
||||
lstream.on('end', function() {
|
||||
cb(), cb = function() {};
|
||||
});
|
||||
|
||||
stream.abort = function() {
|
||||
if (lstream.abort) {
|
||||
lstream.abort();
|
||||
}
|
||||
cb(), cb = function() {};
|
||||
};
|
||||
},
|
||||
// executed after all series
|
||||
function() {
|
||||
// attach a local search results
|
||||
let lstream = self.localStorage.search(startkey, options);
|
||||
stream.abort = function() {
|
||||
lstream.abort();
|
||||
};
|
||||
lstream.pipe(stream, {end: true});
|
||||
lstream.on('error', function(err) {
|
||||
self.logger.error({err: err}, 'search error: @{err.message}');
|
||||
stream.end();
|
||||
});
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve only private local packages
|
||||
* @param {*} callback
|
||||
*/
|
||||
get_local(callback) {
|
||||
let self = this;
|
||||
let locals = this.localStorage.localList.get();
|
||||
let packages = [];
|
||||
|
||||
const getPackage = function(i) {
|
||||
self.localStorage.getPackageMetadata(locals[i], function(err, info) {
|
||||
if (_.isNil(err)) {
|
||||
const latest = info['dist-tags'].latest;
|
||||
|
||||
if (latest && info.versions[latest]) {
|
||||
packages.push(info.versions[latest]);
|
||||
} else {
|
||||
self.logger.warn( {package: locals[i]}, 'package @{package} does not have a "latest" tag?' );
|
||||
}
|
||||
}
|
||||
|
||||
if (i >= locals.length - 1) {
|
||||
callback(null, packages);
|
||||
} else {
|
||||
getPackage(i + 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (locals.length) {
|
||||
getPackage(0);
|
||||
} else {
|
||||
callback(null, []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function fetches package metadata from uplinks and synchronizes it with local data
|
||||
if package is available locally, it MUST be provided in pkginfo
|
||||
returns callback(err, result, uplink_errors)
|
||||
* @param {*} name
|
||||
* @param {*} packageInfo
|
||||
* @param {*} options
|
||||
* @param {*} callback
|
||||
*/
|
||||
_syncUplinksMetadata(name, packageInfo, options, callback) {
|
||||
let exists = false;
|
||||
const self = this;
|
||||
const upLinks = [];
|
||||
|
||||
if (_.isNil(packageInfo)) {
|
||||
exists = false;
|
||||
packageInfo = getDefaultMetadata(name);
|
||||
} else {
|
||||
exists = true;
|
||||
}
|
||||
|
||||
|
||||
for (let up in this.uplinks) {
|
||||
if (this.config.hasProxyTo(name, up)) {
|
||||
upLinks.push(this.uplinks[up]);
|
||||
}
|
||||
}
|
||||
|
||||
async.map(upLinks, (upLink, cb) => {
|
||||
|
||||
const _options = Object.assign({}, options);
|
||||
let upLinkMeta = packageInfo._uplinks[upLink.upname];
|
||||
|
||||
if (Utils.is_object(upLinkMeta)) {
|
||||
|
||||
const fetched = upLinkMeta.fetched;
|
||||
|
||||
if (fetched && (Date.now() - fetched) < upLink.maxage) {
|
||||
return cb();
|
||||
}
|
||||
|
||||
_options.etag = upLinkMeta.etag;
|
||||
}
|
||||
|
||||
upLink.getRemoteMetadata(name, _options, (err, upLinkResponse, eTag) => {
|
||||
if (err && err.remoteStatus === 304) {
|
||||
upLinkMeta.fetched = Date.now();
|
||||
}
|
||||
|
||||
if (err || !upLinkResponse) {
|
||||
return cb(null, [err || Error('no data')]);
|
||||
}
|
||||
|
||||
try {
|
||||
Utils.validate_metadata(upLinkResponse, name);
|
||||
} catch(err) {
|
||||
self.logger.error({
|
||||
sub: 'out',
|
||||
err: err,
|
||||
}, 'package.json validating error @{!err.message}\n@{err.stack}');
|
||||
return cb(null, [err]);
|
||||
}
|
||||
|
||||
packageInfo._uplinks[upLink.upname] = {
|
||||
etag: eTag,
|
||||
fetched: Date.now(),
|
||||
};
|
||||
|
||||
// added to fix verdaccio#73
|
||||
if ('time' in upLinkResponse) {
|
||||
packageInfo['time'] = upLinkResponse.time;
|
||||
}
|
||||
|
||||
this._updateVersionsHiddenUpLink(upLinkResponse.versions, upLink);
|
||||
|
||||
try {
|
||||
Storage._merge_versions(packageInfo, upLinkResponse, self.config);
|
||||
} catch(err) {
|
||||
self.logger.error({
|
||||
sub: 'out',
|
||||
err: err,
|
||||
}, 'package.json parsing error @{!err.message}\n@{err.stack}');
|
||||
return cb(null, [err]);
|
||||
}
|
||||
|
||||
// if we got to this point, assume that the correct package exists
|
||||
// on the uplink
|
||||
exists = true;
|
||||
cb();
|
||||
});
|
||||
}, (err, upLinksErrors) => {
|
||||
assert(!err && Array.isArray(upLinksErrors));
|
||||
|
||||
if (!exists) {
|
||||
return callback( Error[404]('no such package available')
|
||||
, null
|
||||
, upLinksErrors );
|
||||
}
|
||||
|
||||
self.localStorage.updateVersions(name, packageInfo, function(err, packageJsonLocal) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, packageJsonLocal, upLinksErrors);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a hidden value for each version.
|
||||
* @param {Array} versions list of version
|
||||
* @param {String} upLink uplink name
|
||||
* @private
|
||||
*/
|
||||
_updateVersionsHiddenUpLink(versions, upLink) {
|
||||
for (let i in versions) {
|
||||
if (Object.prototype.hasOwnProperty.call(versions, i)) {
|
||||
const version = versions[i];
|
||||
|
||||
// holds a "hidden" value to be used by the package storage.
|
||||
version[Symbol.for('__verdaccio_uplink')] = upLink.upname;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the Up Storage for each link.
|
||||
* @param {Object} config
|
||||
* @private
|
||||
*/
|
||||
_setupUpLinks(config) {
|
||||
// we support a number of uplinks, but only one local storage
|
||||
// Proxy and Local classes should have similar API interfaces
|
||||
this.uplinks = {};
|
||||
for (let p in config.uplinks) {
|
||||
if (Object.prototype.hasOwnProperty.call(config.uplinks, p)) {
|
||||
// instance for each up-link definition
|
||||
this.uplinks[p] = new Proxy(config.uplinks[p], config);
|
||||
this.uplinks[p].upname = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function gets a local info and an info from uplinks and tries to merge it
|
||||
exported for unit tests only.
|
||||
* @param {*} local
|
||||
* @param {*} up
|
||||
* @param {*} config
|
||||
* @static
|
||||
*/
|
||||
static _merge_versions(local, up, config) {
|
||||
// copy new versions to a cache
|
||||
// NOTE: if a certain version was updated, we can't refresh it reliably
|
||||
for (let i in up.versions) {
|
||||
if (_.isNil(local.versions[i])) {
|
||||
local.versions[i] = up.versions[i];
|
||||
}
|
||||
}
|
||||
|
||||
// refresh dist-tags
|
||||
for (let i in up['dist-tags']) {
|
||||
if (local['dist-tags'][i] !== up['dist-tags'][i]) {
|
||||
if (!local['dist-tags'][i] || semver.lte(local['dist-tags'][i], up['dist-tags'][i])) {
|
||||
local['dist-tags'][i] = up['dist-tags'][i];
|
||||
}
|
||||
if (i === 'latest' && local['dist-tags'][i] === up['dist-tags'][i]) {
|
||||
// if remote has more fresh package, we should borrow its readme
|
||||
local.readme = up.readme;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Storage;
|
||||
135
src/lib/storage/local/local-data.js
Normal file
135
src/lib/storage/local/local-data.js
Normal file
@@ -0,0 +1,135 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const Path = require('path');
|
||||
const logger = require('../../logger');
|
||||
|
||||
/**
|
||||
* Handle local database.
|
||||
* FUTURE: must be a plugin.
|
||||
*/
|
||||
class LocalData {
|
||||
|
||||
/**
|
||||
* Load an parse the local json database.
|
||||
* @param {*} path the database path
|
||||
*/
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
// Prevent any write action, wait admin to check what happened during startup
|
||||
this.locked = false;
|
||||
this.data = this._fetchLocalPackages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch local packages.
|
||||
* @private
|
||||
* @return {Object}
|
||||
*/
|
||||
_fetchLocalPackages() {
|
||||
const emptyDatabase = {list: []};
|
||||
|
||||
try {
|
||||
const dbFile = fs.readFileSync(this.path, 'utf8');
|
||||
|
||||
if (!dbFile) { // readFileSync is platform specific, FreeBSD might return null
|
||||
return emptyDatabase;
|
||||
}
|
||||
|
||||
const db = this._parseDatabase(dbFile);
|
||||
|
||||
if(!db) {
|
||||
return emptyDatabase;
|
||||
}
|
||||
|
||||
return db;
|
||||
} catch (err) {
|
||||
// readFileSync is platform specific, macOS, Linux and Windows thrown an error
|
||||
// Only recreate if file not found to prevent data loss
|
||||
if (err.code !== 'ENOENT') {
|
||||
this.locked = true;
|
||||
logger.logger.error(
|
||||
'Failed to read package database file, please check the error printed below:\n',
|
||||
`File Path: ${this.path}\n\n`,
|
||||
err
|
||||
);
|
||||
}
|
||||
return emptyDatabase;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the local database.
|
||||
* @param {Object} dbFile
|
||||
* @private
|
||||
* @return {Object}
|
||||
*/
|
||||
_parseDatabase(dbFile) {
|
||||
try {
|
||||
return JSON.parse(dbFile);
|
||||
} catch(err) {
|
||||
logger.logger.error(`Package database file corrupted (invalid JSON), please check the error printed below.\nFile Path: ${this.path}`, err);
|
||||
this.locked = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new element.
|
||||
* @param {*} name
|
||||
* @return {Error|*}
|
||||
*/
|
||||
add(name) {
|
||||
if (this.data.list.indexOf(name) === -1) {
|
||||
this.data.list.push(name);
|
||||
return this.sync();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an element from the database.
|
||||
* @param {*} name
|
||||
* @return {Error|*}
|
||||
*/
|
||||
remove(name) {
|
||||
const i = this.data.list.indexOf(name);
|
||||
if (i !== -1) {
|
||||
this.data.list.splice(i, 1);
|
||||
}
|
||||
return this.sync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all database elements.
|
||||
* @return {Array}
|
||||
*/
|
||||
get() {
|
||||
return this.data.list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncronize {create} database whether does not exist.
|
||||
* @return {Error|*}
|
||||
*/
|
||||
sync() {
|
||||
if (this.locked) {
|
||||
logger.logger.error('Database is locked, please check error message printed during startup to prevent data loss.');
|
||||
return new Error('Verdaccio database is locked, please contact your administrator to checkout logs during verdaccio startup.');
|
||||
}
|
||||
|
||||
// Uses sync to prevent ugly race condition
|
||||
try {
|
||||
require('mkdirp').sync(Path.dirname(this.path));
|
||||
} catch(err) {
|
||||
// perhaps a logger instance?
|
||||
/* eslint no-empty:off */
|
||||
}
|
||||
try {
|
||||
fs.writeFileSync(this.path, JSON.stringify(this.data));
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = LocalData;
|
||||
256
src/lib/storage/local/local-fs.js
Normal file
256
src/lib/storage/local/local-fs.js
Normal file
@@ -0,0 +1,256 @@
|
||||
/* eslint prefer-spread: "off" */
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const createError = require('http-errors');
|
||||
const mkdirp = require('mkdirp');
|
||||
const MyStream = require('@verdaccio/streams');
|
||||
const locker = require('@verdaccio/file-locking');
|
||||
const fileExist = 'EEXISTS';
|
||||
const noSuchFile = 'ENOENT';
|
||||
|
||||
const fSError = function(code) {
|
||||
const err = createError(code);
|
||||
err.code = code;
|
||||
return err;
|
||||
};
|
||||
|
||||
const readFile = function(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(name, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const tempFile = function(str) {
|
||||
return `${str}.tmp${String(Math.random()).substr(2)}`;
|
||||
};
|
||||
|
||||
const renameTmp = function(src, dst, _cb) {
|
||||
const cb = function(err) {
|
||||
if (err) {
|
||||
fs.unlink(src, function() {});
|
||||
}
|
||||
_cb(err);
|
||||
};
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
return fs.rename(src, dst, cb);
|
||||
}
|
||||
|
||||
// windows can't remove opened file,
|
||||
// but it seem to be able to rename it
|
||||
const tmp = tempFile(dst);
|
||||
fs.rename(dst, tmp, function(err) {
|
||||
fs.rename(src, dst, cb);
|
||||
if (!err) {
|
||||
fs.unlink(tmp, () => {});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const writeFile = function(dest, data, cb) {
|
||||
const createTempFile = function(cb) {
|
||||
const tempFilePath = tempFile(dest);
|
||||
fs.writeFile(tempFilePath, data, function(err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
renameTmp(tempFilePath, dest, cb);
|
||||
});
|
||||
};
|
||||
|
||||
createTempFile(function(err) {
|
||||
if (err && err.code === noSuchFile) {
|
||||
mkdirp(path.dirname(dest), function(err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
createTempFile(cb);
|
||||
});
|
||||
} else {
|
||||
cb(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const createWriteStream = function(name) {
|
||||
const uploadStream = new MyStream.UploadTarball();
|
||||
let _ended = 0;
|
||||
uploadStream.on('end', function() {
|
||||
_ended = 1;
|
||||
});
|
||||
|
||||
fs.exists(name, function(exists) {
|
||||
if (exists) {
|
||||
return uploadStream.emit('error', fSError(fileExist));
|
||||
}
|
||||
|
||||
const temporalName = `${name}.tmp-${String(Math.random()).replace(/^0\./, '')}`;
|
||||
const file = fs.createWriteStream(temporalName);
|
||||
let opened = false;
|
||||
uploadStream.pipe(file);
|
||||
|
||||
uploadStream.done = function() {
|
||||
const onend = function() {
|
||||
file.on('close', function() {
|
||||
renameTmp(temporalName, name, function(err) {
|
||||
if (err) {
|
||||
uploadStream.emit('error', err);
|
||||
} else {
|
||||
uploadStream.emit('success');
|
||||
}
|
||||
});
|
||||
});
|
||||
file.destroySoon();
|
||||
};
|
||||
if (_ended) {
|
||||
onend();
|
||||
} else {
|
||||
uploadStream.on('end', onend);
|
||||
}
|
||||
};
|
||||
uploadStream.abort = function() {
|
||||
if (opened) {
|
||||
opened = false;
|
||||
file.on('close', function() {
|
||||
fs.unlink(temporalName, function() {});
|
||||
});
|
||||
}
|
||||
file.destroySoon();
|
||||
};
|
||||
file.on('open', function() {
|
||||
opened = true;
|
||||
// re-emitting open because it's handled in storage.js
|
||||
uploadStream.emit('open');
|
||||
});
|
||||
file.on('error', function(err) {
|
||||
uploadStream.emit('error', err);
|
||||
});
|
||||
});
|
||||
return uploadStream;
|
||||
};
|
||||
|
||||
const createReadStream = function(name, readTarballStream, callback) {
|
||||
let readStream = fs.createReadStream(name);
|
||||
readStream.on('error', function(err) {
|
||||
readTarballStream.emit('error', err);
|
||||
});
|
||||
readStream.on('open', function(fd) {
|
||||
fs.fstat(fd, function(err, stats) {
|
||||
if (err) return readTarballStream.emit('error', err);
|
||||
readTarballStream.emit('content-length', stats.size);
|
||||
readTarballStream.emit('open');
|
||||
readStream.pipe(readTarballStream);
|
||||
});
|
||||
});
|
||||
|
||||
readTarballStream = new MyStream.ReadTarball();
|
||||
readTarballStream.abort = function() {
|
||||
readStream.close();
|
||||
};
|
||||
return readTarballStream;
|
||||
};
|
||||
|
||||
const createFile = function(name, contents, callback) {
|
||||
fs.exists(name, function(exists) {
|
||||
if (exists) {
|
||||
return callback( fSError(fileExist) );
|
||||
}
|
||||
writeFile(name, contents, callback);
|
||||
});
|
||||
};
|
||||
|
||||
const updateFile = function(name, contents, callback) {
|
||||
fs.exists(name, function(exists) {
|
||||
if (!exists) {
|
||||
return callback( fSError(noSuchFile) );
|
||||
}
|
||||
writeFile(name, contents, callback);
|
||||
});
|
||||
};
|
||||
|
||||
const readJSON = function(name, cb) {
|
||||
readFile(name).then(function(res) {
|
||||
let args = [];
|
||||
try {
|
||||
args = [null, JSON.parse(res.toString('utf8'))];
|
||||
} catch(err) {
|
||||
args = [err];
|
||||
}
|
||||
cb.apply(null, args);
|
||||
}, function(err) {
|
||||
return cb(err);
|
||||
});
|
||||
};
|
||||
|
||||
const lock_and_read = function(name, cb) {
|
||||
locker.readFile(name, {lock: true}, function(err, res) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
return cb(null, res);
|
||||
});
|
||||
};
|
||||
|
||||
const lockAndReadJSON = function(name, cb) {
|
||||
locker.readFile(name, {
|
||||
lock: true,
|
||||
parse: true,
|
||||
}, function(err, res) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
return cb(null, res);
|
||||
});
|
||||
};
|
||||
|
||||
const unlock_file = function(name, cb) {
|
||||
locker.unlockFile(name, cb);
|
||||
};
|
||||
|
||||
const createJSON = function(name, value, cb) {
|
||||
createFile(name, JSON.stringify(value, null, '\t'), cb);
|
||||
};
|
||||
|
||||
|
||||
const updateJSON = function(name, value, cb) {
|
||||
updateFile(name, JSON.stringify(value, null, '\t'), cb);
|
||||
};
|
||||
|
||||
|
||||
const writeJSON = function(name, value, cb) {
|
||||
writeFile(name, JSON.stringify(value, null, '\t'), cb);
|
||||
};
|
||||
|
||||
// fs
|
||||
module.exports.unlink = fs.unlink;
|
||||
module.exports.rmdir = fs.rmdir;
|
||||
|
||||
// streams
|
||||
module.exports.createWriteStream = createWriteStream;
|
||||
module.exports.createReadStream = createReadStream;
|
||||
|
||||
// io
|
||||
module.exports.read = readFile;
|
||||
module.exports.write = writeFile;
|
||||
module.exports.update = updateFile;
|
||||
module.exports.create = createFile;
|
||||
|
||||
// json
|
||||
module.exports.readJSON = readJSON;
|
||||
module.exports.lockAndReadJSON = lockAndReadJSON;
|
||||
module.exports.writeJSON = writeJSON;
|
||||
module.exports.updateJSON = updateJSON;
|
||||
module.exports.createJSON = createJSON;
|
||||
|
||||
// lock
|
||||
module.exports.unlock_file = unlock_file;
|
||||
module.exports.lock_and_read = lock_and_read;
|
||||
977
src/lib/storage/local/local-storage.js
Normal file
977
src/lib/storage/local/local-storage.js
Normal file
@@ -0,0 +1,977 @@
|
||||
/* eslint prefer-rest-params: "off" */
|
||||
/* eslint prefer-spread: "off" */
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const Crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const Path = require('path');
|
||||
const Stream = require('stream');
|
||||
const URL = require('url');
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
const fsStorage = require('./local-fs');
|
||||
const LocalData = require('./local-data');
|
||||
const customStream = require('@verdaccio/streams');
|
||||
|
||||
const pkgFileName = 'package.json';
|
||||
const fileExist = 'EEXISTS';
|
||||
const noSuchFile = 'ENOENT';
|
||||
const resourceNotAvailable = 'EAGAIN';
|
||||
|
||||
const generatePackageTemplate = function(name) {
|
||||
return {
|
||||
// standard things
|
||||
'name': name,
|
||||
'versions': {},
|
||||
'dist-tags': {},
|
||||
'time': {},
|
||||
|
||||
// our own object
|
||||
'_distfiles': {},
|
||||
'_attachments': {},
|
||||
'_uplinks': {},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements Storage interface (same for storage.js, local-storage.js, up-storage.js).
|
||||
*/
|
||||
class LocalStorage {
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} config config list of properties
|
||||
* @param {Object} logger reference
|
||||
* @param {Object} utils package utilities
|
||||
*/
|
||||
constructor(config, logger, utils) {
|
||||
this.config = config;
|
||||
this.utils = utils;
|
||||
this.localList = new LocalData(this._buildStoragePath(this.config));
|
||||
this.logger = logger.child({sub: 'fs'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the local database path.
|
||||
* @param {Object} config
|
||||
* @return {string|String|*}
|
||||
* @private
|
||||
*/
|
||||
_buildStoragePath(config) {
|
||||
// FUTURE: the database might be parameterizable from config.yaml
|
||||
return Path.join(Path.resolve(Path.dirname(config.self_path || ''),
|
||||
config.storage,
|
||||
'.sinopia-db.json'
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a package.
|
||||
* @param {*} name
|
||||
* @param {*} info
|
||||
* @param {*} callback
|
||||
* @return {Function}
|
||||
*/
|
||||
addPackage(name, info, callback) {
|
||||
const storage = this._getLocalStorage(name);
|
||||
|
||||
if (!storage) {
|
||||
return callback( this.utils.ErrorCode.get404('this package cannot be added'));
|
||||
}
|
||||
|
||||
storage.createJSON(pkgFileName, generatePackageTemplate(name), (err) => {
|
||||
if (err && err.code === fileExist) {
|
||||
return callback( this.utils.ErrorCode.get409());
|
||||
}
|
||||
|
||||
const latest = this.utils.getLatestVersion(info);
|
||||
|
||||
if (_.isNil(latest) === false && info.versions[latest]) {
|
||||
return callback(null, info.versions[latest]);
|
||||
}
|
||||
return callback();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove package.
|
||||
* @param {*} name
|
||||
* @param {*} callback
|
||||
* @return {Function}
|
||||
*/
|
||||
removePackage(name, callback) {
|
||||
this.logger.info( {name: name}, 'unpublishing @{name} (all)');
|
||||
|
||||
let storage = this._getLocalStorage(name);
|
||||
if (!storage) {
|
||||
return callback( this.utils.ErrorCode.get404());
|
||||
}
|
||||
|
||||
storage.readJSON(pkgFileName, (err, data) => {
|
||||
if (err) {
|
||||
if (err.code === noSuchFile) {
|
||||
return callback( this.utils.ErrorCode.get404());
|
||||
} else {
|
||||
return callback(err);
|
||||
}
|
||||
}
|
||||
this._normalizePackage(data);
|
||||
|
||||
let removeFailed = this.localList.remove(name);
|
||||
if (removeFailed) {
|
||||
// This will happen when database is locked
|
||||
return callback(this.utils.ErrorCode.get422(removeFailed.message));
|
||||
}
|
||||
|
||||
storage.unlink(pkgFileName, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const files = Object.keys(data._attachments);
|
||||
|
||||
const unlinkNext = function(cb) {
|
||||
if (files.length === 0) {
|
||||
return cb();
|
||||
}
|
||||
|
||||
let file = files.shift();
|
||||
storage.unlink(file, function() {
|
||||
unlinkNext(cb);
|
||||
});
|
||||
};
|
||||
|
||||
unlinkNext(function() {
|
||||
// try to unlink the directory, but ignore errors because it can fail
|
||||
storage.rmdir('.', function(err) {
|
||||
callback(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize remote package info with the local one
|
||||
* @param {*} name
|
||||
* @param {*} packageInfo
|
||||
* @param {*} callback
|
||||
*/
|
||||
updateVersions(name, packageInfo, callback) {
|
||||
this._readCreatePackage(name, (err, packageLocalJson) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let change = false;
|
||||
for (let versionId in packageInfo.versions) {
|
||||
if (_.isNil(packageLocalJson.versions[versionId])) {
|
||||
const version = packageInfo.versions[versionId];
|
||||
|
||||
// we don't keep readmes for package versions,
|
||||
// only one readme per package
|
||||
delete version.readme;
|
||||
|
||||
change = true;
|
||||
packageLocalJson.versions[versionId] = version;
|
||||
|
||||
if (version.dist && version.dist.tarball) {
|
||||
let filename = URL.parse(version.dist.tarball).pathname.replace(/^.*\//, '');
|
||||
// we do NOT overwrite any existing records
|
||||
if (_.isNil(packageLocalJson._distfiles[filename])) {
|
||||
let hash = packageLocalJson._distfiles[filename] = {
|
||||
url: version.dist.tarball,
|
||||
sha: version.dist.shasum,
|
||||
};
|
||||
|
||||
const upLink = version[Symbol.for('__verdaccio_uplink')];
|
||||
|
||||
if (_.isNil(upLink) === false) {
|
||||
hash = this._updateUplinkToRemoteProtocol(hash, upLink);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let tag in packageInfo['dist-tags']) {
|
||||
if (!packageLocalJson['dist-tags'][tag] || packageLocalJson['dist-tags'][tag] !== packageInfo['dist-tags'][tag]) {
|
||||
change = true;
|
||||
packageLocalJson['dist-tags'][tag] = packageInfo['dist-tags'][tag];
|
||||
}
|
||||
}
|
||||
for (let up in packageInfo._uplinks) {
|
||||
if (Object.prototype.hasOwnProperty.call(packageInfo._uplinks, up)) {
|
||||
const need_change = !this.utils.is_object(packageLocalJson._uplinks[up])
|
||||
|| packageInfo._uplinks[up].etag !== packageLocalJson._uplinks[up].etag
|
||||
|| packageInfo._uplinks[up].fetched !== packageLocalJson._uplinks[up].fetched;
|
||||
|
||||
if (need_change) {
|
||||
change = true;
|
||||
packageLocalJson._uplinks[up] = packageInfo._uplinks[up];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (packageInfo.readme !== packageLocalJson.readme) {
|
||||
packageLocalJson.readme = packageInfo.readme;
|
||||
change = true;
|
||||
}
|
||||
|
||||
if ('time' in packageInfo) {
|
||||
packageLocalJson.time = packageInfo.time;
|
||||
change = true;
|
||||
}
|
||||
|
||||
if (change) {
|
||||
this.logger.debug('updating package info');
|
||||
this._writePackage(name, packageLocalJson, function(err) {
|
||||
callback(err, packageLocalJson);
|
||||
});
|
||||
} else {
|
||||
callback(null, packageLocalJson);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the dist file remains as the same protocol
|
||||
* @param {Object} hash metadata
|
||||
* @param {String} upLink registry key
|
||||
* @private
|
||||
*/
|
||||
_updateUplinkToRemoteProtocol(hash, upLink) {
|
||||
// if we got this information from a known registry,
|
||||
// use the same protocol for the tarball
|
||||
//
|
||||
// see https://github.com/rlidwka/sinopia/issues/166
|
||||
const tarballUrl = URL.parse(hash.url);
|
||||
const uplinkUrl = URL.parse(this.config.uplinks[upLink].url);
|
||||
|
||||
if (uplinkUrl.host === tarballUrl.host) {
|
||||
tarballUrl.protocol = uplinkUrl.protocol;
|
||||
hash.registry = upLink;
|
||||
hash.url = URL.format(tarballUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new version to a previous local package.
|
||||
* @param {*} name
|
||||
* @param {*} version
|
||||
* @param {*} metadata
|
||||
* @param {*} tag
|
||||
* @param {*} callback
|
||||
*/
|
||||
addVersion(name, version, metadata, tag, callback) {
|
||||
this._updatePackage(name, (data, cb) => {
|
||||
// keep only one readme per package
|
||||
data.readme = metadata.readme;
|
||||
delete metadata.readme;
|
||||
|
||||
if (data.versions[version] != null) {
|
||||
return cb( this.utils.ErrorCode.get409() );
|
||||
}
|
||||
|
||||
// if uploaded tarball has a different shasum, it's very likely that we have some kind of error
|
||||
if (this.utils.is_object(metadata.dist) && _.isString(metadata.dist.tarball)) {
|
||||
let tarball = metadata.dist.tarball.replace(/.*\//, '');
|
||||
|
||||
if (this.utils.is_object(data._attachments[tarball])) {
|
||||
|
||||
if (_.isNil(data._attachments[tarball].shasum) === false && _.isNil(metadata.dist.shasum) === false) {
|
||||
if (data._attachments[tarball].shasum != metadata.dist.shasum) {
|
||||
const errorMessage = `shasum error, ${data._attachments[tarball].shasum} != ${metadata.dist.shasum}`;
|
||||
return cb( this.utils.ErrorCode.get400(errorMessage) );
|
||||
}
|
||||
}
|
||||
|
||||
let currentDate = new Date().toISOString();
|
||||
data.time['modified'] = currentDate;
|
||||
|
||||
if (('created' in data.time) === false) {
|
||||
data.time.created = currentDate;
|
||||
}
|
||||
|
||||
data.time[version] = currentDate;
|
||||
data._attachments[tarball].version = version;
|
||||
}
|
||||
}
|
||||
|
||||
data.versions[version] = metadata;
|
||||
this.utils.tag_version(data, version, tag);
|
||||
|
||||
let addFailed = this.localList.add(name);
|
||||
if (addFailed) {
|
||||
return cb(this.utils.ErrorCode.get422(addFailed.message));
|
||||
}
|
||||
|
||||
cb();
|
||||
}, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a new list of tags for a local packages with the existing one.
|
||||
* @param {*} name
|
||||
* @param {*} tags
|
||||
* @param {*} callback
|
||||
*/
|
||||
mergeTags(name, tags, callback) {
|
||||
this._updatePackage(name, (data, cb) => {
|
||||
for (let t in tags) {
|
||||
if (tags[t] === null) {
|
||||
delete data['dist-tags'][t];
|
||||
continue;
|
||||
}
|
||||
// be careful here with == (cast)
|
||||
if (_.isNil(data.versions[tags[t]])) {
|
||||
return cb( this._getVersionNotFound() );
|
||||
}
|
||||
|
||||
this.utils.tag_version(data, tags[t], t);
|
||||
}
|
||||
cb();
|
||||
}, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return version not found
|
||||
* @return {String}
|
||||
* @private
|
||||
*/
|
||||
_getVersionNotFound() {
|
||||
return this.utils.ErrorCode.get404('this version doesn\'t exist');
|
||||
}
|
||||
/**
|
||||
* Return file no available
|
||||
* @return {String}
|
||||
* @private
|
||||
*/
|
||||
_getFileNotAvailable() {
|
||||
return this.utils.ErrorCode.get404('no such file available');
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the complete list of tags for a local package.
|
||||
* @param {*} name
|
||||
* @param {*} tags
|
||||
* @param {*} callback
|
||||
*/
|
||||
replaceTags(name, tags, callback) {
|
||||
this._updatePackage(name, (data, cb) => {
|
||||
data['dist-tags'] = {};
|
||||
|
||||
for (let t in tags) {
|
||||
if (_.isNull(tags[t])) {
|
||||
delete data['dist-tags'][t];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_.isNil(data.versions[tags[t]])) {
|
||||
return cb( this._getVersionNotFound() );
|
||||
}
|
||||
|
||||
this.utils.tag_version(data, tags[t], t);
|
||||
}
|
||||
cb();
|
||||
}, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the package metadata, tags and attachments (tarballs).
|
||||
* Note: Currently supports unpublishing only.
|
||||
* @param {*} name
|
||||
* @param {*} metadata
|
||||
* @param {*} revision
|
||||
* @param {*} callback
|
||||
* @return {Function}
|
||||
*/
|
||||
changePackage(name, metadata, revision, callback) {
|
||||
if (!this.utils.is_object(metadata.versions) || !this.utils.is_object(metadata['dist-tags'])) {
|
||||
return callback( this.utils.ErrorCode.get422());
|
||||
}
|
||||
|
||||
this._updatePackage(name, (data, cb) => {
|
||||
for (let ver in data.versions) {
|
||||
if (_.isNil(metadata.versions[ver])) {
|
||||
this.logger.info( {name: name, version: ver},
|
||||
'unpublishing @{name}@@{version}');
|
||||
delete data.versions[ver];
|
||||
for (let file in data._attachments) {
|
||||
if (data._attachments[file].version === ver) {
|
||||
delete data._attachments[file].version;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
data['dist-tags'] = metadata['dist-tags'];
|
||||
cb();
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a tarball.
|
||||
* @param {*} name
|
||||
* @param {*} filename
|
||||
* @param {*} revision
|
||||
* @param {*} callback
|
||||
*/
|
||||
removeTarball(name, filename, revision, callback) {
|
||||
assert(this.utils.validate_name(filename));
|
||||
|
||||
this._updatePackage(name, (data, cb) => {
|
||||
if (data._attachments[filename]) {
|
||||
delete data._attachments[filename];
|
||||
cb();
|
||||
} else {
|
||||
cb(this._getFileNotAvailable());
|
||||
}
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const storage = this._getLocalStorage(name);
|
||||
|
||||
if (storage) {
|
||||
storage.unlink(filename, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tarball.
|
||||
* @param {String} name
|
||||
* @param {String} filename
|
||||
* @return {Stream}
|
||||
*/
|
||||
addTarball(name, filename) {
|
||||
assert(this.utils.validate_name(filename));
|
||||
|
||||
let length = 0;
|
||||
const shaOneHash = Crypto.createHash('sha1');
|
||||
const uploadStream = new customStream.UploadTarball();
|
||||
const _transform = uploadStream._transform;
|
||||
const storage = this._getLocalStorage(name);
|
||||
uploadStream.abort = function() {};
|
||||
uploadStream.done = function() {};
|
||||
|
||||
uploadStream._transform = function(data) {
|
||||
shaOneHash.update(data);
|
||||
// measure the length for validation reasons
|
||||
length += data.length;
|
||||
_transform.apply(uploadStream, arguments);
|
||||
};
|
||||
|
||||
if (name === pkgFileName || name === '__proto__') {
|
||||
process.nextTick(function() {
|
||||
uploadStream.emit('error', this.utils.ErrorCode.get403());
|
||||
});
|
||||
return uploadStream;
|
||||
}
|
||||
|
||||
if (!storage) {
|
||||
process.nextTick(() => {
|
||||
uploadStream.emit('error', ('can\'t upload this package'));
|
||||
});
|
||||
return uploadStream;
|
||||
}
|
||||
|
||||
const writeStream = storage.createWriteStream(filename);
|
||||
|
||||
writeStream.on('error', (err) => {
|
||||
if (err.code === fileExist) {
|
||||
uploadStream.emit('error', this.utils.ErrorCode.get409());
|
||||
} else if (err.code === noSuchFile) {
|
||||
// check if package exists to throw an appropriate message
|
||||
this.getPackageMetadata(name, function(_err, res) {
|
||||
if (_err) {
|
||||
uploadStream.emit('error', _err);
|
||||
} else {
|
||||
uploadStream.emit('error', err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
uploadStream.emit('error', err);
|
||||
}
|
||||
});
|
||||
|
||||
writeStream.on('open', function() {
|
||||
// re-emitting open because it's handled in storage.js
|
||||
uploadStream.emit('open');
|
||||
});
|
||||
|
||||
writeStream.on('success', () => {
|
||||
this._updatePackage(name, function updater(data, cb) {
|
||||
data._attachments[filename] = {
|
||||
shasum: shaOneHash.digest('hex'),
|
||||
};
|
||||
cb();
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
uploadStream.emit('error', err);
|
||||
} else {
|
||||
uploadStream.emit('success');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
uploadStream.abort = function() {
|
||||
writeStream.abort();
|
||||
};
|
||||
|
||||
uploadStream.done = function() {
|
||||
if (!length) {
|
||||
uploadStream.emit('error', this.utils.ErrorCode.get422('refusing to accept zero-length file'));
|
||||
writeStream.abort();
|
||||
} else {
|
||||
writeStream.done();
|
||||
}
|
||||
};
|
||||
|
||||
uploadStream.pipe(writeStream);
|
||||
|
||||
return uploadStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a tarball.
|
||||
* @param {*} name
|
||||
* @param {*} filename
|
||||
* @return {ReadTarball}
|
||||
*/
|
||||
getTarball(name, filename) {
|
||||
assert(this.utils.validate_name(filename));
|
||||
|
||||
const storage = this._getLocalStorage(name);
|
||||
|
||||
if (_.isNil(storage)) {
|
||||
return this._createFailureStreamResponse();
|
||||
}
|
||||
|
||||
return this._streamSuccessReadTarBall(storage, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a stream that emits a read failure.
|
||||
* @private
|
||||
* @return {ReadTarball}
|
||||
*/
|
||||
_createFailureStreamResponse() {
|
||||
const stream = new customStream.ReadTarball();
|
||||
|
||||
process.nextTick(() => {
|
||||
stream.emit('error', this._getFileNotAvailable());
|
||||
});
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a stream that emits the tarball data
|
||||
* @param {Object} storage
|
||||
* @param {String} filename
|
||||
* @private
|
||||
* @return {ReadTarball}
|
||||
*/
|
||||
_streamSuccessReadTarBall(storage, filename) {
|
||||
const stream = new customStream.ReadTarball();
|
||||
const readTarballStream = storage.createReadStream(filename);
|
||||
const e404 = this.utils.ErrorCode.get404;
|
||||
|
||||
stream.abort = function() {
|
||||
if (_.isNil(readTarballStream) === false) {
|
||||
readTarballStream.abort();
|
||||
}
|
||||
};
|
||||
|
||||
readTarballStream.on('error', function(err) {
|
||||
if (err && err.code === noSuchFile) {
|
||||
stream.emit('error', e404('no such file available'));
|
||||
} else {
|
||||
stream.emit('error', err);
|
||||
}
|
||||
});
|
||||
|
||||
readTarballStream.on('content-length', function(v) {
|
||||
stream.emit('content-length', v);
|
||||
});
|
||||
|
||||
readTarballStream.on('open', function() {
|
||||
// re-emitting open because it's handled in storage.js
|
||||
stream.emit('open');
|
||||
readTarballStream.pipe(stream);
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a package by name.
|
||||
* @param {*} name
|
||||
* @param {*} options
|
||||
* @param {*} callback
|
||||
* @return {Function}
|
||||
*/
|
||||
getPackageMetadata(name, options, callback) {
|
||||
if (_.isFunction(options)) {
|
||||
callback = options || {};
|
||||
}
|
||||
|
||||
const storage = this._getLocalStorage(name);
|
||||
if (_.isNil(storage)) {
|
||||
return callback( this.utils.ErrorCode.get404() );
|
||||
}
|
||||
|
||||
this.readJSON(storage, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a json file from storage.
|
||||
* @param {Object} storage
|
||||
* @param {Function} callback
|
||||
*/
|
||||
readJSON(storage, callback) {
|
||||
storage.readJSON(pkgFileName, (err, result) => {
|
||||
if (err) {
|
||||
if (err.code === noSuchFile) {
|
||||
return callback( this.utils.ErrorCode.get404() );
|
||||
} else {
|
||||
return callback(this._internalError(err, pkgFileName, 'error reading'));
|
||||
}
|
||||
}
|
||||
this._normalizePackage(result);
|
||||
callback(err, result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search a local package.
|
||||
* @param {*} startKey
|
||||
* @param {*} options
|
||||
* @return {Function}
|
||||
*/
|
||||
search(startKey, options) {
|
||||
const stream = new Stream.PassThrough({objectMode: true});
|
||||
|
||||
this._eachPackage((item, cb) => {
|
||||
fs.stat(item.path, (err, stats) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if (stats.mtime > startKey) {
|
||||
this.getPackageMetadata(item.name, options, (err, data) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const versions = this.utils.semver_sort(Object.keys(data.versions));
|
||||
const latest = data['dist-tags'] && data['dist-tags'].latest ? data['dist-tags'].latest : versions.pop();
|
||||
|
||||
if (data.versions[latest]) {
|
||||
const version = data.versions[latest];
|
||||
stream.push({
|
||||
'name': version.name,
|
||||
'description': version.description,
|
||||
'dist-tags': {latest: latest},
|
||||
'maintainers': version.maintainers || [version.author].filter(Boolean),
|
||||
'author': version.author,
|
||||
'repository': version.repository,
|
||||
'readmeFilename': version.readmeFilename || '',
|
||||
'homepage': version.homepage,
|
||||
'keywords': version.keywords,
|
||||
'bugs': version.bugs,
|
||||
'license': version.license,
|
||||
'time': {
|
||||
modified: item.time ? new Date(item.time).toISOString() : stats.mtime,
|
||||
},
|
||||
'versions': {[latest]: 'latest'},
|
||||
});
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}, function on_end(err) {
|
||||
if (err) return stream.emit('error', err);
|
||||
stream.end();
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a wrapper that provide access to the package location.
|
||||
* @param {Object} packageInfo package name.
|
||||
* @return {Object}
|
||||
*/
|
||||
_getLocalStorage(packageInfo) {
|
||||
const path = this.__getLocalStoragePath(this.config.getMatchedPackagesSpec(packageInfo).storage);
|
||||
|
||||
if (_.isNil(path) || path === false) {
|
||||
this.logger.debug( {name: packageInfo}, 'this package has no storage defined: @{name}' );
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PathWrapper(
|
||||
Path.join(
|
||||
Path.resolve(Path.dirname(this.config.self_path || ''), path),
|
||||
packageInfo
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the right local storage location.
|
||||
* @param {String} path
|
||||
* @return {String}
|
||||
* @private
|
||||
*/
|
||||
__getLocalStoragePath(path) {
|
||||
if (_.isNil(path)) {
|
||||
path = this.config.storage;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks through each package and calls `on_package` on them.
|
||||
* @param {*} onPackage
|
||||
* @param {*} on_end
|
||||
*/
|
||||
_eachPackage(onPackage, on_end) {
|
||||
let storages = {};
|
||||
let utils = this.utils;
|
||||
|
||||
storages[this.config.storage] = true;
|
||||
if (this.config.packages) {
|
||||
Object.keys(this.config.packages || {}).map( (pkg) => {
|
||||
if (this.config.packages[pkg].storage) {
|
||||
storages[this.config.packages[pkg].storage] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
const base = Path.dirname(this.config.self_path);
|
||||
|
||||
async.eachSeries(Object.keys(storages), function(storage, cb) {
|
||||
fs.readdir(Path.resolve(base, storage), function(err, files) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
async.eachSeries(files, function(file, cb) {
|
||||
if (file.match(/^@/)) {
|
||||
// scoped
|
||||
fs.readdir(Path.resolve(base, storage, file), function(err, files) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
async.eachSeries(files, (file2, cb) => {
|
||||
if (utils.validate_name(file2)) {
|
||||
onPackage({
|
||||
name: `${file}/${file2}`,
|
||||
path: Path.resolve(base, storage, file, file2),
|
||||
}, cb);
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}, cb);
|
||||
});
|
||||
} else if (utils.validate_name(file)) {
|
||||
onPackage({
|
||||
name: file,
|
||||
path: Path.resolve(base, storage, file),
|
||||
}, cb);
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}, cb);
|
||||
});
|
||||
}, on_end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise package properties, tags, revision id.
|
||||
* @param {Object} pkg package reference.
|
||||
*/
|
||||
_normalizePackage(pkg) {
|
||||
const pkgProperties = ['versions', 'dist-tags', '_distfiles', '_attachments', '_uplinks', 'time'];
|
||||
|
||||
pkgProperties.forEach((key) => {
|
||||
if (!this.utils.is_object(pkg[key])) {
|
||||
pkg[key] = {};
|
||||
}
|
||||
});
|
||||
|
||||
if (_.isString(pkg._rev) === false) {
|
||||
pkg._rev = '0-0000000000000000';
|
||||
}
|
||||
// normalize dist-tags
|
||||
this.utils.normalize_dist_tags(pkg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve either a previous created local package or a boilerplate.
|
||||
* @param {*} name
|
||||
* @param {*} callback
|
||||
* @return {Function}
|
||||
*/
|
||||
_readCreatePackage(name, callback) {
|
||||
const storage = this._getLocalStorage(name);
|
||||
if (!storage) {
|
||||
const data = generatePackageTemplate(name);
|
||||
this._normalizePackage(data);
|
||||
return callback(null, data);
|
||||
}
|
||||
storage.readJSON(pkgFileName, (err, data) => {
|
||||
// TODO: race condition
|
||||
if (err) {
|
||||
if (err.code === noSuchFile) {
|
||||
// if package doesn't exist, we create it here
|
||||
data = generatePackageTemplate(name);
|
||||
} else {
|
||||
return callback(this._internalError(err, pkgFileName, 'error reading'));
|
||||
}
|
||||
}
|
||||
this._normalizePackage(data);
|
||||
callback(null, data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle internal error
|
||||
* @param {*} err
|
||||
* @param {*} file
|
||||
* @param {*} message
|
||||
* @return {Object} Error instance
|
||||
*/
|
||||
_internalError(err, file, message) {
|
||||
this.logger.error( {err: err, file: file},
|
||||
message + ' @{file}: @{!err.message}' );
|
||||
return this.utils.ErrorCode.get500();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function allows to update the package thread-safely
|
||||
Algorithm:
|
||||
1. lock package.json for writing
|
||||
2. read package.json
|
||||
3. updateFn(pkg, cb), and wait for cb
|
||||
4. write package.json.tmp
|
||||
5. move package.json.tmp package.json
|
||||
6. callback(err?)
|
||||
* @param {*} name package name
|
||||
* @param {*} updateFn function(package, cb) - update function
|
||||
* @param {*} _callback callback that gets invoked after it's all updated
|
||||
* @return {Function}
|
||||
*/
|
||||
_updatePackage(name, updateFn, _callback) {
|
||||
const storage = this._getLocalStorage(name);
|
||||
if (!storage) {
|
||||
return _callback( this.utils.ErrorCode.get404() );
|
||||
}
|
||||
storage.lockAndReadJSON(pkgFileName, (err, json) => {
|
||||
let locked = false;
|
||||
|
||||
// callback that cleans up lock first
|
||||
const callback = function(err) {
|
||||
let _args = arguments;
|
||||
if (locked) {
|
||||
storage.unlock_file(pkgFileName, function() {
|
||||
// ignore any error from the unlock
|
||||
_callback.apply(err, _args);
|
||||
});
|
||||
} else {
|
||||
_callback.apply(null, _args);
|
||||
}
|
||||
};
|
||||
|
||||
if (!err) {
|
||||
locked = true;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
if (err.code === resourceNotAvailable) {
|
||||
return callback( this.utils.ErrorCode.get503() );
|
||||
} else if (err.code === noSuchFile) {
|
||||
return callback( this.utils.ErrorCode.get404() );
|
||||
} else {
|
||||
return callback(err);
|
||||
}
|
||||
}
|
||||
|
||||
this._normalizePackage(json);
|
||||
updateFn(json, (err) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
this._writePackage(name, json, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the revision (_rev) string for a package.
|
||||
* @param {*} name
|
||||
* @param {*} json
|
||||
* @param {*} callback
|
||||
* @return {Function}
|
||||
*/
|
||||
_writePackage(name, json, callback) {
|
||||
// calculate revision a la couchdb
|
||||
if (typeof(json._rev) !== 'string') {
|
||||
json._rev = '0-0000000000000000';
|
||||
}
|
||||
const rev = json._rev.split('-');
|
||||
json._rev = ((+rev[0] || 0) + 1) + '-' + Crypto.pseudoRandomBytes(8).toString('hex');
|
||||
|
||||
let storage = this._getLocalStorage(name);
|
||||
if (!storage) {
|
||||
return callback();
|
||||
}
|
||||
storage.writeJSON(pkgFileName, json, callback);
|
||||
}
|
||||
}
|
||||
|
||||
const PathWrapper = (function() {
|
||||
/**
|
||||
* A wrapper adding paths to fs_storage methods.
|
||||
*/
|
||||
class Wrapper {
|
||||
|
||||
/**
|
||||
* @param {*} path
|
||||
*/
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
|
||||
const wrapLocalStorageMethods = function(method) {
|
||||
return function() {
|
||||
let args = Array.prototype.slice.apply(arguments);
|
||||
/* eslint no-invalid-this: off */
|
||||
args[0] = Path.join(this.path, args[0] || '');
|
||||
return fsStorage[method].apply(null, args);
|
||||
};
|
||||
};
|
||||
|
||||
for (let i in fsStorage) {
|
||||
if (fsStorage.hasOwnProperty(i)) {
|
||||
Wrapper.prototype[i] = wrapLocalStorageMethods(i);
|
||||
}
|
||||
}
|
||||
|
||||
return Wrapper;
|
||||
})();
|
||||
|
||||
module.exports = LocalStorage;
|
||||
531
src/lib/storage/up-storage.js
Normal file
531
src/lib/storage/up-storage.js
Normal file
@@ -0,0 +1,531 @@
|
||||
'use strict';
|
||||
|
||||
const JSONStream = require('JSONStream');
|
||||
const createError = require('http-errors');
|
||||
const _ = require('lodash');
|
||||
const request = require('request');
|
||||
const Stream = require('stream');
|
||||
const URL = require('url');
|
||||
const Logger = require('../logger');
|
||||
const MyStreams = require('@verdaccio/streams');
|
||||
const Utils = require('../utils');
|
||||
const zlib = require('zlib');
|
||||
|
||||
const encode = function(thing) {
|
||||
return encodeURIComponent(thing).replace(/^%40/, '@');
|
||||
};
|
||||
|
||||
const jsonContentType = 'application/json';
|
||||
|
||||
const contenTypeAccept = `${jsonContentType}; q=0.8, */*`;
|
||||
|
||||
/**
|
||||
* Just a helper (`config[key] || default` doesn't work because of zeroes)
|
||||
* @param {Object} config
|
||||
* @param {Object} key
|
||||
* @param {Object} def
|
||||
* @return {Object}
|
||||
*/
|
||||
const setConfig = (config, key, def) => {
|
||||
return _.isNil(config[key]) === false ? config[key] : def;
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements Storage interface
|
||||
* (same for storage.js, local-storage.js, up-storage.js)
|
||||
*/
|
||||
class ProxyStorage {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {*} config
|
||||
* @param {*} mainConfig
|
||||
*/
|
||||
constructor(config, mainConfig) {
|
||||
this.config = config;
|
||||
this.failed_requests = 0;
|
||||
this.userAgent = mainConfig.user_agent;
|
||||
this.ca = config.ca;
|
||||
this.logger = Logger.logger.child({sub: 'out'});
|
||||
this.server_id = mainConfig.server_id;
|
||||
|
||||
this.url = URL.parse(this.config.url);
|
||||
|
||||
this._setupProxy(this.url.hostname, config, mainConfig, this.url.protocol === 'https:');
|
||||
|
||||
this.config.url = this.config.url.replace(/\/$/, '');
|
||||
|
||||
if (Number(this.config.timeout) >= 1000) {
|
||||
this.logger.warn(['Too big timeout value: ' + this.config.timeout,
|
||||
'We changed time format to nginx-like one',
|
||||
'(see http://nginx.org/en/docs/syntax.html)',
|
||||
'so please update your config accordingly'].join('\n'));
|
||||
}
|
||||
|
||||
// a bunch of different configurable timers
|
||||
this.maxage = Utils.parseInterval(setConfig(this.config, 'maxage', '2m' ));
|
||||
this.timeout = Utils.parseInterval(setConfig(this.config, 'timeout', '30s'));
|
||||
this.max_fails = Number(setConfig(this.config, 'max_fails', 2 ));
|
||||
this.fail_timeout = Utils.parseInterval(setConfig(this.config, 'fail_timeout', '5m' ));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an asset.
|
||||
* @param {*} options
|
||||
* @param {*} cb
|
||||
* @return {Request}
|
||||
*/
|
||||
request(options, cb) {
|
||||
let json;
|
||||
|
||||
if (this._statusCheck() === false) {
|
||||
let streamRead = new Stream.Readable();
|
||||
|
||||
process.nextTick(function() {
|
||||
if (_.isFunction(cb)) {
|
||||
cb(createError('uplink is offline'));
|
||||
}
|
||||
streamRead.emit('error', createError('uplink is offline'));
|
||||
});
|
||||
|
||||
streamRead._read = function() {};
|
||||
// preventing 'Uncaught, unspecified "error" event'
|
||||
streamRead.on('error', function() {});
|
||||
return streamRead;
|
||||
}
|
||||
|
||||
let self = this;
|
||||
let headers = this._setHeaders(options);
|
||||
|
||||
this._addProxyHeaders(options.req, headers);
|
||||
this._overrideWithUplinkConfigHeaders(headers);
|
||||
|
||||
const method = options.method || 'GET';
|
||||
const uri = options.uri_full || (this.config.url + options.uri);
|
||||
|
||||
self.logger.info({
|
||||
method: method,
|
||||
headers: headers,
|
||||
uri: uri,
|
||||
}, 'making request: \'@{method} @{uri}\'');
|
||||
|
||||
if (Utils.is_object(options.json)) {
|
||||
json = JSON.stringify(options.json);
|
||||
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
|
||||
}
|
||||
|
||||
let requestCallback = cb ? (function(err, res, body) {
|
||||
let error;
|
||||
const responseLength = err ? 0 : body.length;
|
||||
|
||||
processBody(err, body);
|
||||
logActivity();
|
||||
cb(err, res, body);
|
||||
|
||||
/**
|
||||
* Perform a decode.
|
||||
*/
|
||||
function processBody() {
|
||||
if (err) {
|
||||
error = err.message;
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.json && res.statusCode < 300) {
|
||||
try {
|
||||
body = JSON.parse(body.toString('utf8'));
|
||||
} catch(_err) {
|
||||
body = {};
|
||||
err = _err;
|
||||
error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
if (!err && Utils.is_object(body)) {
|
||||
if (_.isString(body.error)) {
|
||||
error = body.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Perform a log.
|
||||
*/
|
||||
function logActivity() {
|
||||
let message = '@{!status}, req: \'@{request.method} @{request.url}\'';
|
||||
message += error
|
||||
? ', error: @{!error}'
|
||||
: ', bytes: @{bytes.in}/@{bytes.out}';
|
||||
self.logger.warn({
|
||||
err: err,
|
||||
request: {method: method, url: uri},
|
||||
level: 35, // http
|
||||
status: res != null ? res.statusCode : 'ERR',
|
||||
error: error,
|
||||
bytes: {
|
||||
in: json ? json.length : 0,
|
||||
out: responseLength || 0,
|
||||
},
|
||||
}, message);
|
||||
}
|
||||
}) : undefined;
|
||||
|
||||
const req = request({
|
||||
url: uri,
|
||||
method: method,
|
||||
headers: headers,
|
||||
body: json,
|
||||
ca: this.ca,
|
||||
proxy: this.proxy,
|
||||
encoding: null,
|
||||
gzip: true,
|
||||
timeout: this.timeout,
|
||||
}, requestCallback);
|
||||
|
||||
let statusCalled = false;
|
||||
req.on('response', function(res) {
|
||||
if (!req._verdaccio_aborted && _.isNil(statusCalled) === false) {
|
||||
statusCalled = true;
|
||||
self._statusCheck(true);
|
||||
}
|
||||
|
||||
if (_.isNil(requestCallback) === false) {
|
||||
(function do_log() {
|
||||
const message = '@{!status}, req: \'@{request.method} @{request.url}\' (streaming)';
|
||||
self.logger.warn({
|
||||
request: {
|
||||
method: method,
|
||||
url: uri,
|
||||
},
|
||||
level: 35, // http
|
||||
status: _.isNull(res) === false ? res.statusCode : 'ERR',
|
||||
}, message);
|
||||
})();
|
||||
}
|
||||
});
|
||||
req.on('error', function(_err) {
|
||||
if (!req._verdaccio_aborted && !statusCalled) {
|
||||
statusCalled = true;
|
||||
self._statusCheck(false);
|
||||
}
|
||||
});
|
||||
return req;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default headers.
|
||||
* @param {Object} options
|
||||
* @return {Object}
|
||||
* @private
|
||||
*/
|
||||
_setHeaders(options) {
|
||||
const headers = options.headers || {};
|
||||
const accept = 'Accept';
|
||||
const acceptEncoding = 'Accept-Encoding';
|
||||
const userAgent = 'User-Agent';
|
||||
|
||||
headers[accept] = headers[accept] || contenTypeAccept;
|
||||
headers[acceptEncoding] = headers[acceptEncoding] || 'gzip';
|
||||
// registry.npmjs.org will only return search result if user-agent include string 'npm'
|
||||
headers[userAgent] = headers[userAgent] || `npm (${this.userAgent})`;
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* It will add or override specified headers from config file.
|
||||
*
|
||||
* Eg:
|
||||
*
|
||||
* uplinks:
|
||||
npmjs:
|
||||
url: https://registry.npmjs.org/
|
||||
headers:
|
||||
Accept: "application/vnd.npm.install-v2+json; q=1.0"
|
||||
verdaccio-staging:
|
||||
url: https://mycompany.com/npm
|
||||
headers:
|
||||
Accept: "application/json"
|
||||
authorization: "Basic YourBase64EncodedCredentials=="
|
||||
|
||||
* @param {Object} headers
|
||||
* @private
|
||||
*/
|
||||
_overrideWithUplinkConfigHeaders(headers) {
|
||||
// add/override headers specified in the config
|
||||
for (let key in this.config.headers) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.config.headers, key)) {
|
||||
headers[key] = this.config.headers[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether can fetch from the provided URL.
|
||||
* @param {*} url
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isUplinkValid(url) {
|
||||
url = URL.parse(url);
|
||||
return url.protocol === this.url.protocol && url.host === this.url.host && url.path.indexOf(this.url.path) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a remote package metadata
|
||||
* @param {*} name package name
|
||||
* @param {*} options request options, eg: eTag.
|
||||
* @param {*} callback
|
||||
*/
|
||||
getRemoteMetadata(name, options, callback) {
|
||||
const headers = {};
|
||||
if (_.isNil(options.etag) === false) {
|
||||
headers['If-None-Match'] = options.etag;
|
||||
headers['Accept'] = contenTypeAccept;
|
||||
}
|
||||
|
||||
this.request({
|
||||
uri: `/${encode(name)}`,
|
||||
json: true,
|
||||
headers: headers,
|
||||
req: options.req,
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (res.statusCode === 404) {
|
||||
return callback( createError[404]('package doesn\'t exist on uplink') );
|
||||
}
|
||||
if (!(res.statusCode >= 200 && res.statusCode < 300)) {
|
||||
const error = createError(`bad status code: ${res.statusCode}`);
|
||||
error.remoteStatus = res.statusCode;
|
||||
return callback(error);
|
||||
}
|
||||
callback(null, body, res.headers.etag);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a tarball from the uplink.
|
||||
* @param {String} url
|
||||
* @return {Stream}
|
||||
*/
|
||||
fetchTarball(url) {
|
||||
const stream = new MyStreams.ReadTarball({});
|
||||
let current_length = 0;
|
||||
let expected_length;
|
||||
|
||||
stream.abort = () => {};
|
||||
const readStream = this.request({
|
||||
uri_full: url,
|
||||
encoding: null,
|
||||
headers: {
|
||||
Accept: contenTypeAccept,
|
||||
},
|
||||
});
|
||||
|
||||
readStream.on('response', function(res) {
|
||||
if (res.statusCode === 404) {
|
||||
return stream.emit('error', createError[404]('file doesn\'t exist on uplink'));
|
||||
}
|
||||
if (!(res.statusCode >= 200 && res.statusCode < 300)) {
|
||||
return stream.emit('error', createError('bad uplink status code: ' + res.statusCode));
|
||||
}
|
||||
if (res.headers['content-length']) {
|
||||
expected_length = res.headers['content-length'];
|
||||
stream.emit('content-length', res.headers['content-length']);
|
||||
}
|
||||
|
||||
readStream.pipe(stream);
|
||||
});
|
||||
|
||||
readStream.on('error', function(err) {
|
||||
stream.emit('error', err);
|
||||
});
|
||||
readStream.on('data', function(data) {
|
||||
current_length += data.length;
|
||||
});
|
||||
readStream.on('end', function(data) {
|
||||
if (data) {
|
||||
current_length += data.length;
|
||||
}
|
||||
if (expected_length && current_length != expected_length) {
|
||||
stream.emit('error', createError('content length mismatch'));
|
||||
}
|
||||
});
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a stream search.
|
||||
* @param {*} options request options
|
||||
* @return {Stream}
|
||||
*/
|
||||
search(options) {
|
||||
const transformStream = new Stream.PassThrough({objectMode: true});
|
||||
const requestStream = this.request({
|
||||
uri: options.req.url,
|
||||
req: options.req,
|
||||
headers: {
|
||||
referer: options.req.headers.referer,
|
||||
},
|
||||
});
|
||||
|
||||
let parsePackage = (pkg) => {
|
||||
if (Utils.is_object(pkg)) {
|
||||
transformStream.emit('data', pkg);
|
||||
}
|
||||
};
|
||||
|
||||
requestStream.on('response', (res) => {
|
||||
if (!String(res.statusCode).match(/^2\d\d$/)) {
|
||||
return transformStream.emit('error', createError(`bad status code ${res.statusCode} from uplink`));
|
||||
}
|
||||
|
||||
// See https://github.com/request/request#requestoptions-callback
|
||||
// Request library will not decode gzip stream.
|
||||
let jsonStream;
|
||||
if (res.headers['content-encoding'] === 'gzip') {
|
||||
jsonStream = res.pipe(zlib.createUnzip());
|
||||
} else {
|
||||
jsonStream = res;
|
||||
}
|
||||
jsonStream.pipe(JSONStream.parse('*')).on('data', parsePackage);
|
||||
jsonStream.on('end', () => {
|
||||
transformStream.emit('end');
|
||||
});
|
||||
});
|
||||
|
||||
requestStream.on('error', (err) => {
|
||||
transformStream.emit('error', err);
|
||||
});
|
||||
|
||||
transformStream.abort = () => {
|
||||
requestStream.abort();
|
||||
transformStream.emit('end');
|
||||
};
|
||||
|
||||
return transformStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add proxy headers.
|
||||
* @param {*} req the http request
|
||||
* @param {*} headers the request headers
|
||||
*/
|
||||
_addProxyHeaders(req, headers) {
|
||||
if (req) {
|
||||
// Only submit X-Forwarded-For field if we don't have a proxy selected
|
||||
// in the config file.
|
||||
//
|
||||
// Otherwise misconfigured proxy could return 407:
|
||||
// https://github.com/rlidwka/sinopia/issues/254
|
||||
//
|
||||
if (this.proxy === false) {
|
||||
headers['X-Forwarded-For'] = (
|
||||
req && req.headers['x-forwarded-for']
|
||||
? req.headers['x-forwarded-for'] + ', '
|
||||
: ''
|
||||
) + req.connection.remoteAddress;
|
||||
}
|
||||
}
|
||||
|
||||
// always attach Via header to avoid loops, even if we're not proxying
|
||||
headers['Via'] =
|
||||
req && req.headers['via']
|
||||
? req.headers['via'] + ', '
|
||||
: '';
|
||||
|
||||
headers['Via'] += '1.1 ' + this.server_id + ' (Verdaccio)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the remote host is available.
|
||||
* @param {*} alive
|
||||
* @return {Boolean}
|
||||
*/
|
||||
_statusCheck(alive) {
|
||||
if (arguments.length === 0) {
|
||||
return this._ifRequestFailure() === false;
|
||||
} else {
|
||||
if (alive) {
|
||||
if (this.failed_requests >= this.max_fails) {
|
||||
this.logger.warn({
|
||||
host: this.url.host,
|
||||
}, 'host @{host} is back online');
|
||||
}
|
||||
this.failed_requests = 0;
|
||||
} else {
|
||||
this.failed_requests ++;
|
||||
if (this.failed_requests === this.max_fails) {
|
||||
this.logger.warn({
|
||||
host: this.url.host,
|
||||
}, 'host @{host} is now offline');
|
||||
}
|
||||
}
|
||||
this.last_request_time = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the request failure.
|
||||
* @return {boolean}
|
||||
* @private
|
||||
*/
|
||||
_ifRequestFailure() {
|
||||
return this.failed_requests >= this.max_fails && Math.abs(Date.now() - this.last_request_time) < this.fail_timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a proxy.
|
||||
* @param {*} hostname
|
||||
* @param {*} config
|
||||
* @param {*} mainconfig
|
||||
* @param {*} isHTTPS
|
||||
*/
|
||||
_setupProxy(hostname, config, mainconfig, isHTTPS) {
|
||||
let noProxyList;
|
||||
let proxy_key = isHTTPS ? 'https_proxy' : 'http_proxy';
|
||||
|
||||
// get http_proxy and no_proxy configs
|
||||
if (proxy_key in config) {
|
||||
this.proxy = config[proxy_key];
|
||||
} else if (proxy_key in mainconfig) {
|
||||
this.proxy = mainconfig[proxy_key];
|
||||
}
|
||||
if ('no_proxy' in config) {
|
||||
noProxyList = config.no_proxy;
|
||||
} else if ('no_proxy' in mainconfig) {
|
||||
noProxyList = mainconfig.no_proxy;
|
||||
}
|
||||
|
||||
// use wget-like algorithm to determine if proxy shouldn't be used
|
||||
if (hostname[0] !== '.') {
|
||||
hostname = '.' + hostname;
|
||||
}
|
||||
if (_.isString(noProxyList) && noProxyList.length) {
|
||||
noProxyList = noProxyList.split(',');
|
||||
}
|
||||
if (_.isArray(noProxyList)) {
|
||||
for (let i = 0; i < noProxyList.length; i++) {
|
||||
let noProxyItem = noProxyList[i];
|
||||
if (noProxyItem[0] !== '.') noProxyItem = '.' + noProxyItem;
|
||||
if (hostname.lastIndexOf(noProxyItem) === hostname.length - noProxyItem.length) {
|
||||
if (this.proxy) {
|
||||
this.logger.debug({url: this.url.href, rule: noProxyItem},
|
||||
'not using proxy for @{url}, excluded by @{rule} rule');
|
||||
this.proxy = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if it's non-string (i.e. "false"), don't use it
|
||||
if (_.isString(this.proxy) === false) {
|
||||
delete this.proxy;
|
||||
} else {
|
||||
this.logger.debug( {url: this.url.href, proxy: this.proxy}, 'using proxy @{proxy} for @{url}' );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ProxyStorage;
|
||||
370
src/lib/utils.js
Normal file
370
src/lib/utils.js
Normal file
@@ -0,0 +1,370 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const semver = require('semver');
|
||||
const YAML = require('js-yaml');
|
||||
const URL = require('url');
|
||||
const fs = require('fs');
|
||||
const _ = require('lodash');
|
||||
const Logger = require('./logger');
|
||||
const createError = require('http-errors');
|
||||
|
||||
/**
|
||||
* Validate a package.
|
||||
* @param {*} name
|
||||
* @return {Boolean} whether the package is valid or not
|
||||
*/
|
||||
function validate_package(name) {
|
||||
name = name.split('/', 2);
|
||||
if (name.length === 1) {
|
||||
// normal package
|
||||
return module.exports.validate_name(name[0]);
|
||||
} else {
|
||||
// scoped package
|
||||
return name[0][0] === '@'
|
||||
&& module.exports.validate_name(name[0].slice(1))
|
||||
&& module.exports.validate_name(name[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* From normalize-package-data/lib/fixer.js
|
||||
* @param {*} name the package name
|
||||
* @return {Boolean} whether is valid or not
|
||||
*/
|
||||
function validate_name(name) {
|
||||
if (_.isString(name) === false) {
|
||||
return false;
|
||||
}
|
||||
name = name.toLowerCase();
|
||||
|
||||
// all URL-safe characters and "@" for issue #75
|
||||
return !(!name.match(/^[-a-zA-Z0-9_.!~*'()@]+$/)
|
||||
|| name.charAt(0) === '.' // ".bin", etc.
|
||||
|| name.charAt(0) === '-' // "-" is reserved by couchdb
|
||||
|| name === 'node_modules'
|
||||
|| name === '__proto__'
|
||||
|| name === 'package.json'
|
||||
|| name === 'favicon.ico'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an element is an Object
|
||||
* @param {*} obj the element
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function isObject(obj) {
|
||||
return _.isObject(obj) && _.isNull(obj) === false && _.isArray(obj) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the package metadata, add additional properties whether are missing within
|
||||
* the metadata properties.
|
||||
* @param {*} object
|
||||
* @param {*} name
|
||||
* @return {Object} the object with additional properties as dist-tags ad versions
|
||||
*/
|
||||
function validate_metadata(object, name) {
|
||||
assert(isObject(object), 'not a json object');
|
||||
assert.equal(object.name, name);
|
||||
|
||||
if (!isObject(object['dist-tags'])) {
|
||||
object['dist-tags'] = {};
|
||||
}
|
||||
|
||||
if (!isObject(object['versions'])) {
|
||||
object['versions'] = {};
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create base url for registry.
|
||||
* @param {String} protocol
|
||||
* @param {String} host
|
||||
* @param {String} prefix
|
||||
* @return {String} base registry url
|
||||
*/
|
||||
function combineBaseUrl(protocol, host, prefix) {
|
||||
let result = `${protocol}://${host}`;
|
||||
|
||||
if (prefix) {
|
||||
prefix = prefix.replace(/\/$/, '');
|
||||
|
||||
result = (prefix.indexOf('/') === 0)
|
||||
? `${result}${prefix}`
|
||||
: prefix;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate a packages's versions and filter each original tarbal url.
|
||||
* @param {*} pkg
|
||||
* @param {*} req
|
||||
* @param {*} config
|
||||
* @return {String} a filtered package
|
||||
*/
|
||||
function filter_tarball_urls(pkg, req, config) {
|
||||
/**
|
||||
* Filter a tarball url.
|
||||
* @param {*} _url
|
||||
* @return {String} a parsed url
|
||||
*/
|
||||
const filter = function(_url) {
|
||||
if (!req.headers.host) {
|
||||
return _url;
|
||||
}
|
||||
const filename = URL.parse(_url).pathname.replace(/^.*\//, '');
|
||||
const base = combineBaseUrl(getWebProtocol(req), req.headers.host, config.url_prefix);
|
||||
|
||||
return `${base}/${pkg.name.replace(/\//g, '%2f')}/-/${filename}`;
|
||||
};
|
||||
|
||||
for (let ver in pkg.versions) {
|
||||
if (Object.prototype.hasOwnProperty.call(pkg.versions, ver)) {
|
||||
const dist = pkg.versions[ver].dist;
|
||||
if (_.isNull(dist) === false && _.isNull(dist.tarball) === false) {
|
||||
dist.tarball = filter(dist.tarball);
|
||||
}
|
||||
}
|
||||
}
|
||||
return pkg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tag for a package
|
||||
* @param {*} data
|
||||
* @param {*} version
|
||||
* @param {*} tag
|
||||
* @return {Boolean} whether a package has been tagged
|
||||
*/
|
||||
function tag_version(data, version, tag) {
|
||||
if (tag) {
|
||||
if (data['dist-tags'][tag] !== version) {
|
||||
if (semver.parse(version, true)) {
|
||||
// valid version - store
|
||||
data['dist-tags'][tag] = version;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Logger.logger.warn({ver: version, tag: tag}, 'ignoring bad version @{ver} in @{tag}');
|
||||
if (tag && data['dist-tags'][tag]) {
|
||||
delete data['dist-tags'][tag];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets version from a package object taking into account semver weirdness.
|
||||
* @param {*} object
|
||||
* @param {*} version
|
||||
* @return {String} return the semantic version of a package
|
||||
*/
|
||||
function get_version(object, version) {
|
||||
// this condition must allow cast
|
||||
if (object.versions[version] != null) {
|
||||
return object.versions[version];
|
||||
}
|
||||
try {
|
||||
version = semver.parse(version, true);
|
||||
for (let k in object.versions) {
|
||||
if (version.compare(semver.parse(k, true)) === 0) {
|
||||
return object.versions[k];
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an internet address
|
||||
* Allow:
|
||||
- https:localhost:1234 - protocol + host + port
|
||||
- localhost:1234 - host + port
|
||||
- 1234 - port
|
||||
- http::1234 - protocol + port
|
||||
- https://localhost:443/ - full url + https
|
||||
- http://[::1]:443/ - ipv6
|
||||
- unix:/tmp/http.sock - unix sockets
|
||||
- https://unix:/tmp/http.sock - unix sockets (https)
|
||||
* @param {*} urlAddress the internet address definition
|
||||
* @return {Object|Null} literal object that represent the address parsed
|
||||
*/
|
||||
function parse_address(urlAddress) {
|
||||
//
|
||||
// TODO: refactor it to something more reasonable?
|
||||
//
|
||||
// protocol : // ( host )|( ipv6 ): port /
|
||||
let urlPattern = /^((https?):(\/\/)?)?((([^\/:]*)|\[([^\[\]]+)\]):)?(\d+)\/?$/.exec(urlAddress);
|
||||
|
||||
if (urlPattern) {
|
||||
return {
|
||||
proto: urlPattern[2] || 'http',
|
||||
host: urlPattern[6] || urlPattern[7] || 'localhost',
|
||||
port: urlPattern[8] || '4873',
|
||||
};
|
||||
}
|
||||
|
||||
urlPattern = /^((https?):(\/\/)?)?unix:(.*)$/.exec(urlAddress);
|
||||
|
||||
if (urlPattern) {
|
||||
return {
|
||||
proto: urlPattern[2] || 'http',
|
||||
path: urlPattern[4],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function filters out bad semver versions and sorts the array.
|
||||
* @param {*} array
|
||||
* @return {Array} sorted Array
|
||||
*/
|
||||
function semverSort(array) {
|
||||
return array
|
||||
.filter(function(x) {
|
||||
if (!semver.parse(x, true)) {
|
||||
Logger.logger.warn( {ver: x}, 'ignoring bad version @{ver}' );
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort(semver.compareLoose)
|
||||
.map(String);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten arrays of tags.
|
||||
* @param {*} data
|
||||
*/
|
||||
function normalize_dist_tags(data) {
|
||||
let sorted;
|
||||
if (!data['dist-tags'].latest) {
|
||||
// overwrite latest with highest known version based on semver sort
|
||||
sorted = semverSort(Object.keys(data.versions));
|
||||
if (sorted && sorted.length) {
|
||||
data['dist-tags'].latest = sorted.pop();
|
||||
}
|
||||
}
|
||||
|
||||
for (let tag in data['dist-tags']) {
|
||||
if (_.isArray(data['dist-tags'][tag])) {
|
||||
if (data['dist-tags'][tag].length) {
|
||||
// sort array
|
||||
sorted = semverSort(data['dist-tags'][tag]);
|
||||
if (sorted.length) {
|
||||
// use highest version based on semver sort
|
||||
data['dist-tags'][tag] = sorted.pop();
|
||||
}
|
||||
} else {
|
||||
delete data['dist-tags'][tag];
|
||||
}
|
||||
} else if (_.isString(data['dist-tags'][tag] )) {
|
||||
if (!semver.parse(data['dist-tags'][tag], true)) {
|
||||
// if the version is invalid, delete the dist-tag entry
|
||||
delete data['dist-tags'][tag];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parseIntervalTable = {
|
||||
'': 1000,
|
||||
'ms': 1,
|
||||
's': 1000,
|
||||
'm': 60*1000,
|
||||
'h': 60*60*1000,
|
||||
'd': 86400000,
|
||||
'w': 7*86400000,
|
||||
'M': 30*86400000,
|
||||
'y': 365*86400000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse an internal string to number
|
||||
* @param {*} interval
|
||||
* @return {Number}
|
||||
*/
|
||||
function parseInterval(interval) {
|
||||
if (typeof(interval) === 'number') {
|
||||
return interval * 1000;
|
||||
}
|
||||
let result = 0;
|
||||
let last_suffix = Infinity;
|
||||
interval.split(/\s+/).forEach(function(x) {
|
||||
if (!x) return;
|
||||
let m = x.match(/^((0|[1-9][0-9]*)(\.[0-9]+)?)(ms|s|m|h|d|w|M|y|)$/);
|
||||
if (!m
|
||||
|| parseIntervalTable[m[4]] >= last_suffix
|
||||
|| (m[4] === '' && last_suffix !== Infinity)) {
|
||||
throw Error('invalid interval: ' + interval);
|
||||
}
|
||||
last_suffix = parseIntervalTable[m[4]];
|
||||
result += Number(m[1]) * parseIntervalTable[m[4]];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect running protocol (http or https)
|
||||
* @param {*} req
|
||||
* @return {String}
|
||||
*/
|
||||
function getWebProtocol(req) {
|
||||
return req.get('X-Forwarded-Proto') || req.protocol;
|
||||
}
|
||||
|
||||
const getLatestVersion = function(pkgInfo) {
|
||||
return pkgInfo['dist-tags'].latest;
|
||||
};
|
||||
|
||||
const ErrorCode = {
|
||||
get409: () => {
|
||||
return createError(409, 'this package is already present');
|
||||
},
|
||||
get422: (customMessage) => {
|
||||
return createError(422, customMessage || 'bad data');
|
||||
},
|
||||
get400: (customMessage) => {
|
||||
return createError(400, customMessage);
|
||||
},
|
||||
get500: () => {
|
||||
return createError(500);
|
||||
},
|
||||
get403: () => {
|
||||
return createError(403, 'can\'t use this filename');
|
||||
},
|
||||
get503: () => {
|
||||
return createError(500, 'resource temporarily unavailable');
|
||||
},
|
||||
get404: (customMessage) => {
|
||||
return createError(404, customMessage || 'no such package available');
|
||||
},
|
||||
};
|
||||
|
||||
const parseConfigFile = (config_path) => YAML.safeLoad(fs.readFileSync(config_path, 'utf8'));
|
||||
|
||||
module.exports.parseInterval = parseInterval;
|
||||
module.exports.semver_sort = semverSort;
|
||||
module.exports.parse_address = parse_address;
|
||||
module.exports.get_version = get_version;
|
||||
module.exports.normalize_dist_tags = normalize_dist_tags;
|
||||
module.exports.tag_version = tag_version;
|
||||
module.exports.combineBaseUrl = combineBaseUrl;
|
||||
module.exports.filter_tarball_urls = filter_tarball_urls;
|
||||
module.exports.validate_metadata = validate_metadata;
|
||||
module.exports.is_object = isObject;
|
||||
module.exports.validate_name = validate_name;
|
||||
module.exports.validate_package = validate_package;
|
||||
module.exports.getWebProtocol = getWebProtocol;
|
||||
module.exports.getLatestVersion = getLatestVersion;
|
||||
module.exports.ErrorCode = ErrorCode;
|
||||
module.exports.parseConfigFile = parseConfigFile;
|
||||
59
src/plugins/htpasswd/crypt3.js
Normal file
59
src/plugins/htpasswd/crypt3.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/* eslint require-jsdoc: off */
|
||||
|
||||
'use strict';
|
||||
|
||||
/** Node.js Crypt(3) Library
|
||||
|
||||
Inspired by (and intended to be compatible with) sendanor/crypt3
|
||||
|
||||
see https://github.com/sendanor/node-crypt3
|
||||
|
||||
The key difference is the removal of the dependency on the unix crypt(3) function
|
||||
which is not platform independent, and requires compilation. Instead, a pure
|
||||
javascript version is used.
|
||||
|
||||
*/
|
||||
|
||||
const crypt = require('unix-crypt-td-js');
|
||||
const crypto = require('crypto');
|
||||
|
||||
function createSalt(type) {
|
||||
type = type || 'sha512';
|
||||
|
||||
switch (type) {
|
||||
|
||||
case 'md5':
|
||||
return '$1$' + crypto.randomBytes(10).toString('base64');
|
||||
|
||||
case 'blowfish':
|
||||
return '$2a$' + crypto.randomBytes(10).toString('base64');
|
||||
|
||||
case 'sha256':
|
||||
return '$5$' + crypto.randomBytes(10).toString('base64');
|
||||
|
||||
case 'sha512':
|
||||
return '$6$' + crypto.randomBytes(10).toString('base64');
|
||||
|
||||
default:
|
||||
throw new TypeError('Unknown salt type at crypt3.createSalt: ' + type);
|
||||
}
|
||||
}
|
||||
|
||||
function crypt3(key, salt) {
|
||||
salt = salt || createSalt();
|
||||
return crypt(key, salt);
|
||||
}
|
||||
|
||||
/** Crypt(3) password and data encryption.
|
||||
* @param {string} key user's typed password
|
||||
* @param {string} salt Optional salt, for example SHA-512 use "$6$salt$".
|
||||
* @returns {string} A generated hash in format $id$salt$encrypted
|
||||
* @see https://en.wikipedia.org/wiki/Crypt_(C)
|
||||
*/
|
||||
module.exports = crypt3;
|
||||
|
||||
/** Create salt
|
||||
* @param {string} type The type of salt: md5, blowfish (only some linux distros), sha256 or sha512. Default is sha512.
|
||||
* @returns {string} Generated salt string
|
||||
*/
|
||||
module.exports.createSalt = createSalt;
|
||||
137
src/plugins/htpasswd/index.js
Normal file
137
src/plugins/htpasswd/index.js
Normal file
@@ -0,0 +1,137 @@
|
||||
/* eslint require-jsdoc: off */
|
||||
|
||||
'use strict';
|
||||
|
||||
let fs = require('fs');
|
||||
let Path = require('path');
|
||||
let utils = require('./utils');
|
||||
|
||||
module.exports = HTPasswd;
|
||||
|
||||
function HTPasswd(config, stuff) {
|
||||
let self = Object.create(HTPasswd.prototype);
|
||||
self._users = {};
|
||||
|
||||
// config for this module
|
||||
self._config = config;
|
||||
|
||||
// verdaccio logger
|
||||
self._logger = stuff.logger;
|
||||
|
||||
// verdaccio main config object
|
||||
self._verdaccio_config = stuff.config;
|
||||
|
||||
// all this "verdaccio_config" stuff is for b/w compatibility only
|
||||
self._maxusers = self._config.max_users;
|
||||
if (!self._maxusers) self._maxusers = self._verdaccio_config.max_users;
|
||||
// set maxusers to Infinity if not specified
|
||||
if (!self._maxusers) self._maxusers = Infinity;
|
||||
|
||||
self._last_time = null;
|
||||
let file = self._config.file;
|
||||
if (!file) file = self._verdaccio_config.users_file;
|
||||
if (!file) throw new Error('should specify "file" in config');
|
||||
self._path = Path.resolve(Path.dirname(self._verdaccio_config.self_path), file);
|
||||
return self;
|
||||
}
|
||||
|
||||
HTPasswd.prototype.authenticate = function(user, password, cb) {
|
||||
let self = this;
|
||||
self._reload(function(err) {
|
||||
if (err) return cb(err.code === 'ENOENT' ? null : err);
|
||||
if (!self._users[user]) return cb(null, false);
|
||||
if (!utils.verify_password(user, password, self._users[user])) return cb(null, false);
|
||||
|
||||
// authentication succeeded!
|
||||
// return all usergroups this user has access to;
|
||||
// (this particular package has no concept of usergroups, so just return user herself)
|
||||
return cb(null, [user]);
|
||||
});
|
||||
};
|
||||
|
||||
// hopefully race-condition-free way to add users:
|
||||
// 1. lock file for writing (other processes can still read)
|
||||
// 2. reload .htpasswd
|
||||
// 3. write new data into .htpasswd.tmp
|
||||
// 4. move .htpasswd.tmp to .htpasswd
|
||||
// 5. reload .htpasswd
|
||||
// 6. unlock file
|
||||
HTPasswd.prototype.adduser = function(user, password, real_cb) {
|
||||
let self = this;
|
||||
|
||||
function sanity_check() {
|
||||
let err = null;
|
||||
if (self._users[user]) {
|
||||
err = Error('this user already exists');
|
||||
} else if (Object.keys(self._users).length >= self._maxusers) {
|
||||
err = Error('maximum amount of users reached');
|
||||
}
|
||||
if (err) err.status = 403;
|
||||
return err;
|
||||
}
|
||||
|
||||
// preliminary checks, just to ensure that file won't be reloaded if it's not needed
|
||||
let s_err = sanity_check();
|
||||
if (s_err) return real_cb(s_err, false);
|
||||
|
||||
utils.lock_and_read(self._path, function(err, res) {
|
||||
let locked = false;
|
||||
|
||||
// callback that cleans up lock first
|
||||
function cb(err) {
|
||||
if (locked) {
|
||||
utils.unlock_file(self._path, function() {
|
||||
// ignore any error from the unlock
|
||||
real_cb(err, !err);
|
||||
});
|
||||
} else {
|
||||
real_cb(err, !err);
|
||||
}
|
||||
}
|
||||
|
||||
if (!err) {
|
||||
locked = true;
|
||||
}
|
||||
|
||||
// ignore ENOENT errors, we'll just create .htpasswd in that case
|
||||
if (err && err.code !== 'ENOENT') return cb(err);
|
||||
|
||||
let body = (res || '').toString('utf8');
|
||||
self._users = utils.parse_htpasswd(body);
|
||||
|
||||
// real checks, to prevent race conditions
|
||||
let s_err = sanity_check();
|
||||
if (s_err) return cb(s_err);
|
||||
|
||||
try {
|
||||
body = utils.add_user_to_htpasswd(body, user, password);
|
||||
} catch (err) {
|
||||
return cb(err);
|
||||
}
|
||||
fs.writeFile(self._path, body, function(err) {
|
||||
if (err) return cb(err);
|
||||
self._reload(function() {
|
||||
cb(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
HTPasswd.prototype._reload = function(_callback) {
|
||||
let self = this;
|
||||
|
||||
fs.stat(self._path, function(err, stats) {
|
||||
if (err) return _callback(err);
|
||||
|
||||
if (self._last_time === stats.mtime) return _callback();
|
||||
self._last_time = stats.mtime;
|
||||
|
||||
fs.readFile(self._path, 'utf8', function(err, buffer) {
|
||||
if (err) return _callback(err);
|
||||
|
||||
self._users = utils.parse_htpasswd(buffer);
|
||||
|
||||
_callback();
|
||||
});
|
||||
});
|
||||
};
|
||||
72
src/plugins/htpasswd/utils.js
Normal file
72
src/plugins/htpasswd/utils.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/* eslint require-jsdoc: off */
|
||||
|
||||
'use strict';
|
||||
|
||||
let crypto = require('crypto');
|
||||
let crypt3 = require('./crypt3');
|
||||
let md5 = require('apache-md5');
|
||||
let locker = require('@verdaccio/file-locking');
|
||||
|
||||
// this function neither unlocks file nor closes it
|
||||
// it'll have to be done manually later
|
||||
function lock_and_read(name, cb) {
|
||||
locker.readFile(name, {lock: true}, function(err, res) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
return cb(null, res);
|
||||
});
|
||||
}
|
||||
|
||||
// close and unlock file
|
||||
function unlock_file(name, cb) {
|
||||
locker.unlockFile(name, cb);
|
||||
}
|
||||
|
||||
function parse_htpasswd(input) {
|
||||
let result = {};
|
||||
input.split('\n').forEach(function(line) {
|
||||
let args = line.split(':', 3);
|
||||
if (args.length > 1) result[args[0]] = args[1];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function verify_password(user, passwd, hash) {
|
||||
if (hash.indexOf('{PLAIN}') === 0) {
|
||||
return passwd === hash.substr(7);
|
||||
} else if (hash.indexOf('{SHA}') === 0) {
|
||||
return crypto.createHash('sha1').update(passwd, 'binary').digest('base64') === hash.substr(5);
|
||||
} else {
|
||||
return (
|
||||
// for backwards compatibility, first check md5 then check crypt3
|
||||
md5(passwd, hash) === hash ||
|
||||
crypt3(passwd, hash) === hash
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function add_user_to_htpasswd(body, user, passwd) {
|
||||
if (user !== encodeURIComponent(user)) {
|
||||
let err = Error('username should not contain non-uri-safe characters');
|
||||
err.status = 409;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (crypt3) {
|
||||
passwd = crypt3(passwd);
|
||||
} else {
|
||||
passwd = '{SHA}' + crypto.createHash('sha1').update(passwd, 'binary').digest('base64');
|
||||
}
|
||||
let comment = 'autocreated ' + (new Date()).toJSON();
|
||||
|
||||
let newline = user + ':' + passwd + ':' + comment + '\n';
|
||||
if (body.length && body[body.length-1] !== '\n') newline = '\n' + newline;
|
||||
return body + newline;
|
||||
}
|
||||
|
||||
module.exports.parse_htpasswd = parse_htpasswd;
|
||||
module.exports.verify_password = verify_password;
|
||||
module.exports.add_user_to_htpasswd = add_user_to_htpasswd;
|
||||
module.exports.lock_and_read = lock_and_read;
|
||||
module.exports.unlock_file = unlock_file;
|
||||
34
src/webui/.eslintrc
Normal file
34
src/webui/.eslintrc
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"jest": true,
|
||||
"es6": true
|
||||
},
|
||||
"rules": {
|
||||
"require-jsdoc": 0,
|
||||
"no-console": [
|
||||
1,
|
||||
{
|
||||
"allow": [
|
||||
"log"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-unused-vars": [
|
||||
2,
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "all"
|
||||
}
|
||||
],
|
||||
"comma-dangle": 0,
|
||||
"semi": 1,
|
||||
"react/no-danger-with-children": 1,
|
||||
"react/no-string-refs": 1,
|
||||
"react/prefer-es6-class": [
|
||||
2,
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
19
src/webui/src/app.js
Normal file
19
src/webui/src/app.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import 'element-theme-default';
|
||||
import {i18n} from 'element-react';
|
||||
import locale from 'element-react/src/locale/lang/en';
|
||||
|
||||
i18n.use(locale);
|
||||
|
||||
import Route from './router';
|
||||
|
||||
import './styles/global.scss';
|
||||
import 'normalize.css';
|
||||
|
||||
export default class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Route/>
|
||||
);
|
||||
}
|
||||
}
|
||||
34
src/webui/src/components/Header/header.scss
Normal file
34
src/webui/src/components/Header/header.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
@import '../../styles/variable';
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
height: 70px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
background: $primary-color;
|
||||
|
||||
figure {
|
||||
margin: 0 0 0 10px;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
padding: 8px 0;
|
||||
color: #f9f2f4;
|
||||
}
|
||||
|
||||
.headerWrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@include container-size();
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
margin-left: auto;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
185
src/webui/src/components/Header/index.js
Normal file
185
src/webui/src/components/Header/index.js
Normal file
@@ -0,0 +1,185 @@
|
||||
import React from 'react';
|
||||
import {Button, Dialog, Input, Alert} from 'element-react';
|
||||
import isString from 'lodash/isString';
|
||||
import get from 'lodash/get';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import API from '../../../utils/api';
|
||||
import storage from '../../../utils/storage';
|
||||
|
||||
|
||||
import classes from './header.scss';
|
||||
import './logo.png';
|
||||
|
||||
export default class Header extends React.Component {
|
||||
state = {
|
||||
showLogin: false,
|
||||
username: '',
|
||||
password: '',
|
||||
logo: '',
|
||||
loginError: null
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.toggleLoginModal = this.toggleLoginModal.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
toggleLoginModal() {
|
||||
this.setState({
|
||||
showLogin: !this.state.showLogin
|
||||
});
|
||||
this.setState({loginError: null});
|
||||
}
|
||||
|
||||
handleInput(name, e) {
|
||||
this.setState({
|
||||
[name]: e
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
API.get('logo')
|
||||
.then((response) => {
|
||||
this.setState({logo: response.data});
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Error(error);
|
||||
});
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
if (this.state.username === '' || this.state.password === '') {
|
||||
return this.setState({loginError: {
|
||||
title: 'Unable to login',
|
||||
type: 'error',
|
||||
description: 'Username or password can\'t be empty!'
|
||||
}});
|
||||
}
|
||||
|
||||
try {
|
||||
let resp = await API.post(`login`, {
|
||||
data: {
|
||||
username: this.state.username,
|
||||
password: this.state.password
|
||||
}
|
||||
});
|
||||
|
||||
storage.setItem('token', resp.data.token);
|
||||
storage.setItem('username', resp.data.username);
|
||||
location.reload();
|
||||
} catch (e) {
|
||||
const errorObj = {
|
||||
title: 'Unable to login',
|
||||
type: 'error'
|
||||
};
|
||||
if (get(e, 'response.status', 0) === 401) {
|
||||
errorObj.description = e.response.data.error;
|
||||
} else {
|
||||
errorObj.description = e.message;
|
||||
}
|
||||
this.setState({loginError: errorObj});
|
||||
}
|
||||
}
|
||||
|
||||
get isTokenExpire() {
|
||||
const token = storage.getItem('token');
|
||||
|
||||
if (!isString(token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let payload = token.split('.')[1];
|
||||
|
||||
if (!payload) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
payload = JSON.parse(atob(payload));
|
||||
} catch (err) {
|
||||
console.error('Invalid token:', err, token); // eslint-disable-line
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!payload.exp || !isNumber(payload.exp)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const jsTimestamp = (payload.exp * 1000) - 30000; // Report as expire before (real expire time - 30s)
|
||||
const expired = Date.now() >= jsTimestamp;
|
||||
|
||||
if (expired) {
|
||||
storage.clear();
|
||||
}
|
||||
|
||||
return expired;
|
||||
}
|
||||
|
||||
handleLogout() {
|
||||
storage.clear();
|
||||
location.reload();
|
||||
}
|
||||
|
||||
renderUserActionButton() {
|
||||
if (!this.isTokenExpire) { // TODO: Check jwt token expire
|
||||
return (
|
||||
<div className={ classes.welcome }>
|
||||
Hi, {storage.getItem('username')}
|
||||
|
||||
<Button type="danger" onClick={this.handleLogout}>Logout</Button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <Button type="danger" style={ {marginLeft: 'auto'} } onClick={ this.toggleLoginModal }>Login</Button>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<header className={ classes.header }>
|
||||
<div className={ classes.headerWrap }>
|
||||
<Link to="/">
|
||||
<img src={ this.state.logo } className={ classes.logo } />
|
||||
</Link>
|
||||
<figure>
|
||||
npm set registry { location.origin }
|
||||
<br/>
|
||||
npm adduser --registry { location.origin }
|
||||
</figure>
|
||||
{this.renderUserActionButton()}
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
title="Login"
|
||||
size="tiny"
|
||||
visible={ this.state.showLogin }
|
||||
onCancel={ () => this.toggleLoginModal() }
|
||||
>
|
||||
<Dialog.Body>
|
||||
{ this.state.loginError &&
|
||||
<Alert
|
||||
title={this.state.loginError.title} type={this.state.loginError.type}
|
||||
description={this.state.loginError.description} showIcon={true} closable={false}>
|
||||
</Alert>
|
||||
}
|
||||
<br/>
|
||||
<Input placeholder="Username" onChange={this.handleInput.bind(this, 'username')} />
|
||||
<br/><br/>
|
||||
<Input type="password" placeholder="Type your password" onChange={this.handleInput.bind(this, 'password')} />
|
||||
</Dialog.Body>
|
||||
<Dialog.Footer className="dialog-footer">
|
||||
<Button onClick={ () => this.toggleLoginModal() }>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={ this.handleSubmit }>
|
||||
Login
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
BIN
src/webui/src/components/Header/logo.png
Normal file
BIN
src/webui/src/components/Header/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
26
src/webui/src/components/Help/help.scss
Normal file
26
src/webui/src/components/Help/help.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
.help {
|
||||
.noPkg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 30px 0;
|
||||
font-size: 20px;
|
||||
color: #9f9f9f;
|
||||
|
||||
.noPkgTitle {
|
||||
margin: 1em 0;
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.5;
|
||||
margin: 0 auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/webui/src/components/Help/index.js
Normal file
40
src/webui/src/components/Help/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
import React from 'react';
|
||||
import SyntaxHighlighter, {registerLanguage} from 'react-syntax-highlighter/dist/light';
|
||||
import sunburst from 'react-syntax-highlighter/src/styles/sunburst';
|
||||
import js from 'react-syntax-highlighter/dist/languages/javascript';
|
||||
|
||||
import classes from './help.scss';
|
||||
|
||||
registerLanguage('javascript', js);
|
||||
|
||||
const Help = () => {
|
||||
return (
|
||||
<div className={classes.help}>
|
||||
<li className={classes.noPkg}>
|
||||
<h1 className={classes.noPkgTitle}>
|
||||
No Package Published Yet
|
||||
</h1>
|
||||
<p>
|
||||
<div>
|
||||
To publish your first package just:
|
||||
</div>
|
||||
<br/>
|
||||
<strong>
|
||||
1. Login
|
||||
</strong>
|
||||
<SyntaxHighlighter language='javascript' style={sunburst}>
|
||||
{`npm adduser --registry ${location.origin}`}
|
||||
</SyntaxHighlighter>
|
||||
<strong>2. Publish</strong>
|
||||
<SyntaxHighlighter language='javascript' style={sunburst}>
|
||||
{`npm publish --registry ${location.origin}`}
|
||||
</SyntaxHighlighter>
|
||||
<strong>3. Refresh this page!</strong>
|
||||
</p>
|
||||
</li>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Help;
|
||||
18
src/webui/src/components/NoItems/index.js
Normal file
18
src/webui/src/components/NoItems/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classes from './noItems.scss';
|
||||
|
||||
const NoItems = (props) => {
|
||||
return (
|
||||
<div className={classes.noItems}>
|
||||
<h2>{props.text}</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NoItems.propTypes = {
|
||||
text: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default NoItems;
|
||||
3
src/webui/src/components/NoItems/noItems.scss
Normal file
3
src/webui/src/components/NoItems/noItems.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.noItems {
|
||||
margin: 5em 0;
|
||||
}
|
||||
14
src/webui/src/components/NotFound/404.scss
Normal file
14
src/webui/src/components/NotFound/404.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
@import '../../styles/variable';
|
||||
|
||||
.notFound {
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
line-height: 30px;
|
||||
border: none;
|
||||
border-bottom: 1px solid lightgrey;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
}
|
||||
23
src/webui/src/components/NotFound/index.js
Normal file
23
src/webui/src/components/NotFound/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classes from './404.scss';
|
||||
|
||||
const NotFound = (props) => {
|
||||
return (
|
||||
<div className={classes.notFound}>
|
||||
<h1>Error 404 - {props.pkg}</h1>
|
||||
<hr/>
|
||||
<p>
|
||||
Oops, The package you are trying to access does not exist.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NotFound.propTypes = {
|
||||
pkg: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
35
src/webui/src/components/Package/index.js
Normal file
35
src/webui/src/components/Package/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Tag} from 'element-react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import isNil from 'lodash/isNil';
|
||||
|
||||
import classes from './package.scss';
|
||||
|
||||
export default class Package extends React.Component {
|
||||
static propTypes = {
|
||||
package: PropTypes.object
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
package: pkg
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link to={`detail/${pkg.name}`} className={classes.package}>
|
||||
<h1>{pkg.name}<Tag type="gray">v{pkg.version}</Tag></h1>
|
||||
{this.renderAuthor(pkg)}
|
||||
<p>{pkg.description}</p>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
renderAuthor(pkg) {
|
||||
if (isNil(pkg.author) || isNil(pkg.author.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <span role="author" className={classes.author}>By: {pkg.author.name}</span>;
|
||||
}
|
||||
}
|
||||
65
src/webui/src/components/Package/package.scss
Normal file
65
src/webui/src/components/Package/package.scss
Normal file
@@ -0,0 +1,65 @@
|
||||
@import '../../styles/variable';
|
||||
|
||||
.package {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
color: $primary-color;
|
||||
|
||||
:global {
|
||||
.el-tag {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Author
|
||||
.author {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 0;
|
||||
color: lightgrey;
|
||||
font-size: inherit;
|
||||
word-wrap: break-word;
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
font-size: 14px;
|
||||
color: darkgray;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 0;
|
||||
left: 0;
|
||||
content: 'Click to view detail';
|
||||
text-align: center;
|
||||
color: $primary-color;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/webui/src/components/PackageDetail/index.js
Normal file
35
src/webui/src/components/PackageDetail/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isNil from 'lodash/isNil';
|
||||
|
||||
import Readme from '../Readme';
|
||||
|
||||
import classes from './packageDetail.scss';
|
||||
|
||||
const PackageDetail = (props) => {
|
||||
|
||||
const displayState = (readMe) => {
|
||||
if (isNil(readMe)) {
|
||||
return;
|
||||
}
|
||||
return <Readme readMe={readMe}/>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.pkgDetail}>
|
||||
<h1 className={ classes.title }>{ props.package }</h1>
|
||||
<hr/>
|
||||
<div className={classes.readme}>
|
||||
{displayState(props.readMe)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PackageDetail.propTypes = {
|
||||
readMe: PropTypes.string,
|
||||
package: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default PackageDetail;
|
||||
12
src/webui/src/components/PackageDetail/packageDetail.scss
Normal file
12
src/webui/src/components/PackageDetail/packageDetail.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
@import '../../styles/variable';
|
||||
|
||||
.pkgDetail {
|
||||
.title {
|
||||
font-size: 28px;
|
||||
color: $text-black;
|
||||
}
|
||||
|
||||
.readme {
|
||||
margin-bottom: 5em;
|
||||
}
|
||||
}
|
||||
65
src/webui/src/components/PackageList/index.js
Normal file
65
src/webui/src/components/PackageList/index.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
import Package from '../Package';
|
||||
import Help from '../Help';
|
||||
import NoItems from '../NoItems';
|
||||
|
||||
import classes from './packageList.scss';
|
||||
|
||||
export default class PackageList extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
packages: PropTypes.array,
|
||||
help: PropTypes.bool
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className={classes.pkgContainer}>
|
||||
{this.renderTitle()}
|
||||
{this.isTherePackages() ? this.renderList(): this.renderOptions()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTitle() {
|
||||
if (this.isTherePackages() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <h1 className={ classes.listTitle }>Available Packages</h1>;
|
||||
}
|
||||
|
||||
renderList() {
|
||||
return this.props.packages.map((pkg, i)=> (
|
||||
<li key={i}><Package package={pkg} /></li>
|
||||
));
|
||||
}
|
||||
|
||||
renderOptions() {
|
||||
if (this.isTherePackages() === false && this.props.help) {
|
||||
return this.renderHelp();
|
||||
} else {
|
||||
return this.renderNoItems();
|
||||
}
|
||||
}
|
||||
|
||||
renderNoItems() {
|
||||
return <NoItems text={'No items were found with that query'}/>;
|
||||
}
|
||||
|
||||
renderHelp() {
|
||||
if (this.props.help === false) {
|
||||
return;
|
||||
}
|
||||
return <Help/>;
|
||||
}
|
||||
|
||||
isTherePackages() {
|
||||
return isEmpty(this.props.packages) === false;
|
||||
}
|
||||
}
|
||||
16
src/webui/src/components/PackageList/packageList.scss
Normal file
16
src/webui/src/components/PackageList/packageList.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.pkgContainer {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid #e4e8f1;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.listTitle {
|
||||
font-weight: normal;
|
||||
font-size: 24px;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
15
src/webui/src/components/Readme/index.js
Normal file
15
src/webui/src/components/Readme/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import 'github-markdown-css';
|
||||
|
||||
const Readme = (props) => {
|
||||
return <div className="markdown-body" dangerouslySetInnerHTML={{__html: props.readMe}}/>;
|
||||
};
|
||||
|
||||
Readme.propTypes = {
|
||||
readMe: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default Readme;
|
||||
14
src/webui/src/components/Readme/readme.scss
Normal file
14
src/webui/src/components/Readme/readme.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
@import '../../styles/variable';
|
||||
|
||||
.searchBox {
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
line-height: 30px;
|
||||
border: none;
|
||||
border-bottom: 1px solid lightgrey;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
}
|
||||
27
src/webui/src/components/Search/index.js
Normal file
27
src/webui/src/components/Search/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classes from './search.scss';
|
||||
|
||||
const Search = (props) => {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
placeholder={props.placeHolder}
|
||||
className={ classes.searchBox }
|
||||
onChange={ props.handleSearchInput }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Search.defaultProps = {
|
||||
placeHolder: 'Type to search...'
|
||||
};
|
||||
|
||||
Search.propTypes = {
|
||||
handleSearchInput: PropTypes.func.isRequired,
|
||||
placeHolder: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Search;
|
||||
14
src/webui/src/components/Search/search.scss
Normal file
14
src/webui/src/components/Search/search.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
@import '../../styles/variable';
|
||||
|
||||
.searchBox {
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
line-height: 30px;
|
||||
border: none;
|
||||
border-bottom: 1px solid lightgrey;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
}
|
||||
26
src/webui/src/index.js
Normal file
26
src/webui/src/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import '../utils/__setPublicPath__';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {AppContainer} from 'react-hot-loader';
|
||||
|
||||
import App from './app';
|
||||
|
||||
let rootNode = document.getElementById('root');
|
||||
|
||||
let renderApp = (Component) => {
|
||||
ReactDOM.render(
|
||||
<AppContainer>
|
||||
<Component/>
|
||||
</AppContainer>,
|
||||
rootNode
|
||||
);
|
||||
};
|
||||
|
||||
renderApp(App);
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./app', () => {
|
||||
renderApp(App);
|
||||
});
|
||||
}
|
||||
2
src/webui/src/modules/detail/detail.scss
Normal file
2
src/webui/src/modules/detail/detail.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@import '../../styles/variable';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user