Compare commits
282 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
972e232559 | ||
|
|
851a91b1a0 | ||
|
|
6a068dc430 | ||
|
|
2cdfcf60fe | ||
|
|
5d9e0c367a | ||
|
|
cbf8263033 | ||
|
|
846c73d9bc | ||
|
|
e0b43b1976 | ||
|
|
6a6e788968 | ||
|
|
4754cad42a | ||
|
|
db58e53f3b | ||
|
|
b31acb4348 | ||
|
|
c794c2c076 | ||
|
|
42d6cd44bb | ||
|
|
f9057ca56a | ||
|
|
c2f1999037 | ||
|
|
44b386f7a7 | ||
|
|
89b296db4e | ||
|
|
c16403fb3f | ||
|
|
5e21fb72e6 | ||
|
|
c5681bf880 | ||
|
|
db85d6545d | ||
|
|
9912c3deba | ||
|
|
fc5c342e40 | ||
|
|
60707b3faa | ||
|
|
f36845ac6b | ||
|
|
9f76bd6cad | ||
|
|
c53d5d9964 | ||
|
|
171d461ea5 | ||
|
|
b2b04a1155 | ||
|
|
671ba03b78 | ||
|
|
9095725778 | ||
|
|
8b2e5daba3 | ||
|
|
75b7a5f571 | ||
|
|
4b9e881ad0 | ||
|
|
59f8f52cca | ||
|
|
4adaf83fd3 | ||
|
|
84464a4ea6 | ||
|
|
cafac0b8b5 | ||
|
|
5346300a64 | ||
|
|
1d4554eabc | ||
|
|
50bdba8b70 | ||
|
|
8c785f6dea | ||
|
|
93e6abc9ba | ||
|
|
60d7c85c11 | ||
|
|
883bad2ee5 | ||
|
|
7d68e144f8 | ||
|
|
7f32e38cf8 | ||
|
|
43a96d1636 | ||
|
|
00e7d2e45d | ||
|
|
2e711c3591 | ||
|
|
5d147fc03b | ||
|
|
3dccefbbcb | ||
|
|
7c4995fa4a | ||
|
|
2b8f051414 | ||
|
|
4e17c81d63 | ||
|
|
8b4b677d6a | ||
|
|
47229bf473 | ||
|
|
ec5b567da9 | ||
|
|
03b59fae1c | ||
|
|
ede8b3ecf2 | ||
|
|
7ae90b9199 | ||
|
|
a651da451e | ||
|
|
f220af5c54 | ||
|
|
eebf987900 | ||
|
|
3d5101011f | ||
|
|
2cdc19dfdd | ||
|
|
d8a7b1e16c | ||
|
|
d7490d089c | ||
|
|
774c6c0f64 | ||
|
|
92d11c53de | ||
|
|
c509f65a27 | ||
|
|
20b9e839d3 | ||
|
|
6f4fb24cd0 | ||
|
|
23a89fe1de | ||
|
|
559df1f523 | ||
|
|
ad2b8ebc38 | ||
|
|
3824629d4d | ||
|
|
38224714e1 | ||
|
|
a9f4227bba | ||
|
|
06bde77f51 | ||
|
|
2a2554e7a3 | ||
|
|
5d835011e6 | ||
|
|
0f294cd62d | ||
|
|
8e62d21c25 | ||
|
|
2a5dd63e87 | ||
|
|
06cb424b8f | ||
|
|
7c5864a9c3 | ||
|
|
668e34ccab | ||
|
|
95477715fc | ||
|
|
ecc004a485 | ||
|
|
9f0657374b | ||
|
|
61b7f72e94 | ||
|
|
db000baaa5 | ||
|
|
a1a3a29d00 | ||
|
|
2ea73a941f | ||
|
|
e860b60d20 | ||
|
|
d6be01b9b7 | ||
|
|
64c8e01c33 | ||
|
|
acce797e55 | ||
|
|
175fc8d68b | ||
|
|
b9d1cf69c7 | ||
|
|
41eeb364f8 | ||
|
|
a22f8f0b7b | ||
|
|
bd1c3609a7 | ||
|
|
c5e75df64f | ||
|
|
6b181dd291 | ||
|
|
4ab88cad10 | ||
|
|
b902953df4 | ||
|
|
e141368734 | ||
|
|
980da40988 | ||
|
|
22d25f1e70 | ||
|
|
84d77d0a9f | ||
|
|
b0afdf933a | ||
|
|
e9eef9a49e | ||
|
|
6f2b58cbdc | ||
|
|
09ac2c35f3 | ||
|
|
47a6fc9906 | ||
|
|
c3d49fde95 | ||
|
|
ec1e4d5c8a | ||
|
|
e65ef7ccc1 | ||
|
|
68e7fd2090 | ||
|
|
b958f8461f | ||
|
|
a08d84e7ed | ||
|
|
2b66d8d56a | ||
|
|
a40789e1f2 | ||
|
|
63571af252 | ||
|
|
75c6840ecd | ||
|
|
e6a02a85f0 | ||
|
|
2c3de75f3d | ||
|
|
7c4aab34ed | ||
|
|
a8480f82e0 | ||
|
|
a5dacca9a1 | ||
|
|
31ba233b34 | ||
|
|
5720123576 | ||
|
|
9cc09b320d | ||
|
|
cb3c1056e5 | ||
|
|
82f96e457c | ||
|
|
062e2b4b8f | ||
|
|
9de51acbcc | ||
|
|
6d3a97cdbc | ||
|
|
3ebcdd9c3d | ||
|
|
a9f86d1d01 | ||
|
|
2a68fc3114 | ||
|
|
2352a53e6e | ||
|
|
fcc94c58d9 | ||
|
|
da9c4920ab | ||
|
|
0295eedb6e | ||
|
|
7f26cc1dbb | ||
|
|
9e1c395810 | ||
|
|
9db4b92d4e | ||
|
|
ff46ee89d9 | ||
|
|
b9af077ef4 | ||
|
|
b23ee4144d | ||
|
|
57f894bfca | ||
|
|
58e2abca8c | ||
|
|
ed676b0d7e | ||
|
|
ed42f343d2 | ||
|
|
2555c478b4 | ||
|
|
6152e55e7d | ||
|
|
023cdd1bb3 | ||
|
|
5efe250466 | ||
|
|
695ddc91dd | ||
|
|
7b30017a14 | ||
|
|
e5542ae266 | ||
|
|
d19b8a53f2 | ||
|
|
2e39f70cd5 | ||
|
|
26c0bb8b1a | ||
|
|
12b0db07da | ||
|
|
7aace9109a | ||
|
|
6c4caea26f | ||
|
|
13c0726849 | ||
|
|
1b52673603 | ||
|
|
7ea7237d19 | ||
|
|
d6bb5e44a1 | ||
|
|
072464928b | ||
|
|
4125bc2768 | ||
|
|
ee29310ed3 | ||
|
|
0e6917ae2a | ||
|
|
57dd2c6c01 | ||
|
|
6494e69632 | ||
|
|
d6ea80dab1 | ||
|
|
fbb3e7134f | ||
|
|
9fdb36c6ca | ||
|
|
9bc545b4bb | ||
|
|
457f60f815 | ||
|
|
78db26a532 | ||
|
|
fb15225c35 | ||
|
|
c8fb103762 | ||
|
|
585fd3a3ff | ||
|
|
0aefd4d093 | ||
|
|
5f0c5b3375 | ||
|
|
6b0b22cb7b | ||
|
|
0f84be8fa0 | ||
|
|
849a485621 | ||
|
|
a721d4ff01 | ||
|
|
62c8fe2dbf | ||
|
|
756c8f02e8 | ||
|
|
62db91b7be | ||
|
|
8543a1f01a | ||
|
|
db20c81a39 | ||
|
|
395827afeb | ||
|
|
8329fd3ab7 | ||
|
|
e32f4200d1 | ||
|
|
7ed60f6908 | ||
|
|
a413aa86c2 | ||
|
|
b51c90a0e9 | ||
|
|
7245508c76 | ||
|
|
905159f044 | ||
|
|
ac8181474c | ||
|
|
554937dd7a | ||
|
|
4aa9a227e8 | ||
|
|
6e4cbf5dd8 | ||
|
|
44d1ac7f11 | ||
|
|
1c32da7751 | ||
|
|
999b740df6 | ||
|
|
6864b7ca10 | ||
|
|
546b350a6c | ||
|
|
d70296cd95 | ||
|
|
10f4ad49d9 | ||
|
|
edb230b278 | ||
|
|
efbf66a0a4 | ||
|
|
0746458762 | ||
|
|
f2738e2bd1 | ||
|
|
b0d8180809 | ||
|
|
f9d450ffaf | ||
|
|
391a39d82c | ||
|
|
7eb76c861f | ||
|
|
cd437a3a7b | ||
|
|
2accc7abd4 | ||
|
|
3c10578584 | ||
|
|
511470087b | ||
|
|
017ee2ab39 | ||
|
|
b093f61fb5 | ||
|
|
bd158819d3 | ||
|
|
86f6e87efe | ||
|
|
e377cac8e6 | ||
|
|
0fbcc0dd41 | ||
|
|
1fdf37dc07 | ||
|
|
4cf73e3410 | ||
|
|
328bb0153b | ||
|
|
1ddd6867b6 | ||
|
|
2becd5eec2 | ||
|
|
571b37da6b | ||
|
|
01d464f4c5 | ||
|
|
bf184c621d | ||
|
|
f4309f843b | ||
|
|
cbdc231cbf | ||
|
|
0f0a8dd9bb | ||
|
|
4189eb8154 | ||
|
|
1d6349767d | ||
|
|
f6ba06298d | ||
|
|
31a8cef59f | ||
|
|
beeba0551b | ||
|
|
a36bb68957 | ||
|
|
9cd9152a91 | ||
|
|
09c1e0504e | ||
|
|
37d7c839dd | ||
|
|
8ba418308e | ||
|
|
cfcf124d83 | ||
|
|
ccb0071d12 | ||
|
|
681f59c1e6 | ||
|
|
1bdee1a107 | ||
|
|
a2f3a51fe5 | ||
|
|
98d4360a76 | ||
|
|
15c58c99b2 | ||
|
|
e6198e16e5 | ||
|
|
97d166ad8f | ||
|
|
fb4ca3d219 | ||
|
|
62aea661cc | ||
|
|
f33326db4d | ||
|
|
c5d00728d0 | ||
|
|
1591cbc208 | ||
|
|
58d9aef616 | ||
|
|
a087cdad09 | ||
|
|
7f754e2ab9 | ||
|
|
2261d27c94 | ||
|
|
f97c9f2878 | ||
|
|
d6a7a6702f | ||
|
|
abba8dc990 | ||
|
|
602514f517 | ||
|
|
8db6afe8b9 |
BIN
.assets/client.gif
Normal file
|
After Width: | Height: | Size: 147 KiB |
30
.assets/cluster-mysql.dot
Normal file
@@ -0,0 +1,30 @@
|
||||
graph {
|
||||
rankdir=LR;
|
||||
subgraph cluster_sshportal {
|
||||
label="sshportal cluster";
|
||||
edge[style=dashed,color=grey,constraint=false];
|
||||
sshportal1; sshportal2; sshportal3; sshportalN;
|
||||
sshportal1 -- MySQL;
|
||||
sshportal2 -- MySQL;
|
||||
sshportal3 -- MySQL;
|
||||
sshportalN -- MySQL;
|
||||
}
|
||||
|
||||
subgraph cluster_hosts {
|
||||
label="hosts";
|
||||
host1; host2; host3; hostN;
|
||||
}
|
||||
|
||||
subgraph cluster_users {
|
||||
label="users";
|
||||
user1; user2; user3; userN;
|
||||
}
|
||||
|
||||
{
|
||||
user1 -- sshportal1 -- host1[color=red,penwidth=3.0];
|
||||
user2 -- sshportal2 -- host2[color=green,penwidth=3.0];
|
||||
user3 -- sshportal3 -- host3[color=blue,penwidth=3.0];
|
||||
user3 -- sshportal2 -- host1[color=purple,penwidth=3.0];
|
||||
userN -- sshportalN -- hostN[style=dotted];
|
||||
}
|
||||
}
|
||||
BIN
.assets/cluster-mysql.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
176
.assets/cluster-mysql.svg
Normal file
@@ -0,0 +1,176 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: %3 Pages: 1 -->
|
||||
<svg width="334pt" height="314pt"
|
||||
viewBox="0.00 0.00 333.78 314.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 310)">
|
||||
<title>%3</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-310 329.7751,-310 329.7751,4 -4,4"/>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_sshportal</title>
|
||||
<polygon fill="none" stroke="#000000" points="106.4533,-8 106.4533,-298 219.3045,-298 219.3045,-8 106.4533,-8"/>
|
||||
<text text-anchor="middle" x="162.8789" y="-282.8" font-family="Times,serif" font-size="14.00" fill="#000000">sshportal cluster</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_hosts</title>
|
||||
<polygon fill="none" stroke="#000000" points="239.3045,-62 239.3045,-298 317.7751,-298 317.7751,-62 239.3045,-62"/>
|
||||
<text text-anchor="middle" x="278.5398" y="-282.8" font-family="Times,serif" font-size="14.00" fill="#000000">hosts</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_users</title>
|
||||
<polygon fill="none" stroke="#000000" points="8,-62 8,-298 86.4533,-298 86.4533,-62 8,-62"/>
|
||||
<text text-anchor="middle" x="47.2266" y="-282.8" font-family="Times,serif" font-size="14.00" fill="#000000">users</text>
|
||||
</g>
|
||||
<!-- sshportal1 -->
|
||||
<g id="node1" class="node">
|
||||
<title>sshportal1</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="162.8789" cy="-250" rx="46.4218" ry="18"/>
|
||||
<text text-anchor="middle" x="162.8789" y="-245.8" font-family="Times,serif" font-size="14.00" fill="#000000">sshportal1</text>
|
||||
</g>
|
||||
<!-- MySQL -->
|
||||
<g id="node5" class="node">
|
||||
<title>MySQL</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="162.8789" cy="-34" rx="39.1973" ry="18"/>
|
||||
<text text-anchor="middle" x="162.8789" y="-29.8" font-family="Times,serif" font-size="14.00" fill="#000000">MySQL</text>
|
||||
</g>
|
||||
<!-- sshportal1--MySQL -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>sshportal1--MySQL</title>
|
||||
<path fill="none" stroke="#c0c0c0" stroke-dasharray="5,2" d="M124.8869,-239.5365C113.0929,-234.0967 101.7898,-225.9823 96.4533,-214 83.4344,-184.768 83.4344,-99.232 96.4533,-70 102.3234,-56.8194 115.4132,-48.3192 128.4343,-42.9106"/>
|
||||
</g>
|
||||
<!-- host1 -->
|
||||
<g id="node6" class="node">
|
||||
<title>host1</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="278.5398" cy="-250" rx="29.0429" ry="18"/>
|
||||
<text text-anchor="middle" x="278.5398" y="-245.8" font-family="Times,serif" font-size="14.00" fill="#000000">host1</text>
|
||||
</g>
|
||||
<!-- sshportal1--host1 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>sshportal1--host1</title>
|
||||
<path fill="none" stroke="#ff0000" stroke-width="3" d="M209.5892,-250C222.8807,-250 236.9866,-250 248.9537,-250"/>
|
||||
</g>
|
||||
<!-- sshportal2 -->
|
||||
<g id="node2" class="node">
|
||||
<title>sshportal2</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="162.8789" cy="-196" rx="46.4218" ry="18"/>
|
||||
<text text-anchor="middle" x="162.8789" y="-191.8" font-family="Times,serif" font-size="14.00" fill="#000000">sshportal2</text>
|
||||
</g>
|
||||
<!-- sshportal2--MySQL -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>sshportal2--MySQL</title>
|
||||
<path fill="none" stroke="#c0c0c0" stroke-dasharray="5,2" d="M124.8869,-185.5365C113.0929,-180.0967 101.7898,-171.9823 96.4533,-160 88.3165,-141.73 88.3165,-88.27 96.4533,-70 102.3234,-56.8194 115.4132,-48.3192 128.4343,-42.9106"/>
|
||||
</g>
|
||||
<!-- sshportal2--host1 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>sshportal2--host1</title>
|
||||
<path fill="none" stroke="#a020f0" stroke-width="3" d="M192.6645,-209.9063C212.0749,-218.9687 237.0472,-230.6278 255.0535,-239.0347"/>
|
||||
</g>
|
||||
<!-- host2 -->
|
||||
<g id="node7" class="node">
|
||||
<title>host2</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="278.5398" cy="-196" rx="29.0429" ry="18"/>
|
||||
<text text-anchor="middle" x="278.5398" y="-191.8" font-family="Times,serif" font-size="14.00" fill="#000000">host2</text>
|
||||
</g>
|
||||
<!-- sshportal2--host2 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>sshportal2--host2</title>
|
||||
<path fill="none" stroke="#00ff00" stroke-width="3" d="M209.5892,-196C222.8807,-196 236.9866,-196 248.9537,-196"/>
|
||||
</g>
|
||||
<!-- sshportal3 -->
|
||||
<g id="node3" class="node">
|
||||
<title>sshportal3</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="162.8789" cy="-142" rx="46.4218" ry="18"/>
|
||||
<text text-anchor="middle" x="162.8789" y="-137.8" font-family="Times,serif" font-size="14.00" fill="#000000">sshportal3</text>
|
||||
</g>
|
||||
<!-- sshportal3--MySQL -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>sshportal3--MySQL</title>
|
||||
<path fill="none" stroke="#c0c0c0" stroke-dasharray="5,2" d="M124.8869,-131.5365C113.0929,-126.0967 101.7898,-117.9823 96.4533,-106 89.9439,-91.384 89.9439,-84.616 96.4533,-70 102.3234,-56.8194 115.4132,-48.3192 128.4343,-42.9106"/>
|
||||
</g>
|
||||
<!-- host3 -->
|
||||
<g id="node8" class="node">
|
||||
<title>host3</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="278.5398" cy="-142" rx="29.0429" ry="18"/>
|
||||
<text text-anchor="middle" x="278.5398" y="-137.8" font-family="Times,serif" font-size="14.00" fill="#000000">host3</text>
|
||||
</g>
|
||||
<!-- sshportal3--host3 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>sshportal3--host3</title>
|
||||
<path fill="none" stroke="#0000ff" stroke-width="3" d="M209.5892,-142C222.8807,-142 236.9866,-142 248.9537,-142"/>
|
||||
</g>
|
||||
<!-- sshportalN -->
|
||||
<g id="node4" class="node">
|
||||
<title>sshportalN</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="162.8789" cy="-88" rx="48.3514" ry="18"/>
|
||||
<text text-anchor="middle" x="162.8789" y="-83.8" font-family="Times,serif" font-size="14.00" fill="#000000">sshportalN</text>
|
||||
</g>
|
||||
<!-- sshportalN--MySQL -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>sshportalN--MySQL</title>
|
||||
<path fill="none" stroke="#c0c0c0" stroke-dasharray="5,2" d="M162.8789,-69.7902C162.8789,-63.907 162.8789,-58.0238 162.8789,-52.1406"/>
|
||||
</g>
|
||||
<!-- hostN -->
|
||||
<g id="node9" class="node">
|
||||
<title>hostN</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="278.5398" cy="-88" rx="31.4723" ry="18"/>
|
||||
<text text-anchor="middle" x="278.5398" y="-83.8" font-family="Times,serif" font-size="14.00" fill="#000000">hostN</text>
|
||||
</g>
|
||||
<!-- sshportalN--hostN -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>sshportalN--hostN</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M211.5943,-88C223.5713,-88 236.0833,-88 247.0054,-88"/>
|
||||
</g>
|
||||
<!-- user1 -->
|
||||
<g id="node10" class="node">
|
||||
<title>user1</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="47.2266" cy="-250" rx="29.0257" ry="18"/>
|
||||
<text text-anchor="middle" x="47.2266" y="-245.8" font-family="Times,serif" font-size="14.00" fill="#000000">user1</text>
|
||||
</g>
|
||||
<!-- user1--sshportal1 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>user1--sshportal1</title>
|
||||
<path fill="none" stroke="#ff0000" stroke-width="3" d="M76.7098,-250C88.7561,-250 102.9825,-250 116.3672,-250"/>
|
||||
</g>
|
||||
<!-- user2 -->
|
||||
<g id="node11" class="node">
|
||||
<title>user2</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="47.2266" cy="-196" rx="29.0257" ry="18"/>
|
||||
<text text-anchor="middle" x="47.2266" y="-191.8" font-family="Times,serif" font-size="14.00" fill="#000000">user2</text>
|
||||
</g>
|
||||
<!-- user2--sshportal2 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>user2--sshportal2</title>
|
||||
<path fill="none" stroke="#00ff00" stroke-width="3" d="M76.7098,-196C88.7561,-196 102.9825,-196 116.3672,-196"/>
|
||||
</g>
|
||||
<!-- user3 -->
|
||||
<g id="node12" class="node">
|
||||
<title>user3</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="47.2266" cy="-142" rx="29.0257" ry="18"/>
|
||||
<text text-anchor="middle" x="47.2266" y="-137.8" font-family="Times,serif" font-size="14.00" fill="#000000">user3</text>
|
||||
</g>
|
||||
<!-- user3--sshportal2 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>user3--sshportal2</title>
|
||||
<path fill="none" stroke="#a020f0" stroke-width="3" d="M70.6306,-152.9277C88.5836,-161.3103 113.4965,-172.9425 132.9075,-182.0059"/>
|
||||
</g>
|
||||
<!-- user3--sshportal3 -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>user3--sshportal3</title>
|
||||
<path fill="none" stroke="#0000ff" stroke-width="3" d="M76.7098,-142C88.7561,-142 102.9825,-142 116.3672,-142"/>
|
||||
</g>
|
||||
<!-- userN -->
|
||||
<g id="node13" class="node">
|
||||
<title>userN</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="47.2266" cy="-88" rx="31.4549" ry="18"/>
|
||||
<text text-anchor="middle" x="47.2266" y="-83.8" font-family="Times,serif" font-size="14.00" fill="#000000">userN</text>
|
||||
</g>
|
||||
<!-- userN--sshportalN -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>userN--sshportalN</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M78.5238,-88C89.4948,-88 102.0923,-88 114.1488,-88"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
BIN
.assets/demo.gif
Normal file
|
After Width: | Height: | Size: 179 KiB |
65
.assets/flow-diagram.dot
Normal file
@@ -0,0 +1,65 @@
|
||||
digraph {
|
||||
node[shape=record;style=rounded;fontname="helvetica-bold"];
|
||||
graph[layout=dot;rankdir=LR;overlap=prism;splines=true;fontname="helvetica-bold"];
|
||||
edge[arrowhead=none;fontname="helvetica"];
|
||||
|
||||
start[label="\$\> ssh sshportal";color=blue;fontcolor=blue;fontsize=18];
|
||||
|
||||
subgraph cluster_sshportal {
|
||||
graph[fontsize=18;color=gray;fontcolor=black];
|
||||
label="sshportal";
|
||||
{
|
||||
node[color=darkorange;fontcolor=darkorange];
|
||||
known_user_key[label="known user key"];
|
||||
unknown_user_key[label="unknown user key"];
|
||||
invite_manager[label="invite manager"];
|
||||
acl_manager[label="ACL manager"];
|
||||
}
|
||||
{
|
||||
node[color=darkgreen;fontcolor=darkgreen];
|
||||
builtin_shell[label="built-in\nconfig shell"];
|
||||
ssh_proxy[label="SSH proxy\nJump-Host"];
|
||||
learn_key[label="learn the\npub key"];
|
||||
}
|
||||
err_and_exit[label="\nerror\nand exit\n\n";color=red;fontcolor=red];
|
||||
{ rank=same; ssh_proxy; builtin_shell; learn_key; err_and_exit; }
|
||||
{ rank=same; known_user_key; unknown_user_key; }
|
||||
}
|
||||
|
||||
subgraph cluster_hosts {
|
||||
label="your hosts";
|
||||
graph[fontsize=18;color=gray;fontcolor=black];
|
||||
node[color=blue;fontcolor=blue];
|
||||
|
||||
host_1[label="root@host1"];
|
||||
host_2[label="user@host2:2222"];
|
||||
host_3[label="root@host3:1234"];
|
||||
}
|
||||
|
||||
{
|
||||
edge[color=blue];
|
||||
start -> known_user_key;
|
||||
start -> unknown_user_key;
|
||||
ssh_proxy -> host_1;
|
||||
ssh_proxy -> host_2;
|
||||
ssh_proxy -> host_3;
|
||||
}
|
||||
{
|
||||
edge[color=darkgreen;fontcolor=darkgreen];
|
||||
known_user_key -> builtin_shell[label="user=admin"];
|
||||
acl_manager -> ssh_proxy[label="authorized"];
|
||||
invite_manager -> learn_key[label="valid token"];
|
||||
}
|
||||
{
|
||||
edge[color=darkorange;fontcolor=darkorange];
|
||||
known_user_key -> acl_manager[label="user matches an existing host"];
|
||||
unknown_user_key -> invite_manager[label="user=invite:<token>";labelloc=b];
|
||||
}
|
||||
{
|
||||
edge[color=red;fontcolor=red];
|
||||
known_user_key -> err_and_exit[label="invalid user"];
|
||||
acl_manager -> err_and_exit[label="unauthorized"];
|
||||
unknown_user_key -> err_and_exit[label="any other user";constraint=false];
|
||||
invite_manager -> err_and_exit[label="invalid token";constraint=false];
|
||||
}
|
||||
}
|
||||
BIN
.assets/flow-diagram.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
178
.assets/flow-diagram.svg
Normal file
@@ -0,0 +1,178 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: %3 Pages: 1 -->
|
||||
<svg width="1150pt" height="310pt"
|
||||
viewBox="0.00 0.00 1149.83 310.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 306)">
|
||||
<title>%3</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-306 1145.8281,-306 1145.8281,4 -4,4"/>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_sshportal</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="187.5586,-8 187.5586,-294 964.46,-294 964.46,-8 187.5586,-8"/>
|
||||
<text text-anchor="middle" x="576.0093" y="-275.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="18.00" fill="#000000">sshportal</text>
|
||||
</g>
|
||||
<g id="clust6" class="cluster">
|
||||
<title>cluster_hosts</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="985.46,-104 985.46,-294 1133.8281,-294 1133.8281,-104 985.46,-104"/>
|
||||
<text text-anchor="middle" x="1059.644" y="-275.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="18.00" fill="#000000">your hosts</text>
|
||||
</g>
|
||||
<!-- start -->
|
||||
<g id="node1" class="node">
|
||||
<title>start</title>
|
||||
<path fill="none" stroke="#0000ff" d="M12,-122C12,-122 146.5586,-122 146.5586,-122 152.5586,-122 158.5586,-128 158.5586,-134 158.5586,-134 158.5586,-146 158.5586,-146 158.5586,-152 152.5586,-158 146.5586,-158 146.5586,-158 12,-158 12,-158 6,-158 0,-152 0,-146 0,-146 0,-134 0,-134 0,-128 6,-122 12,-122"/>
|
||||
<text text-anchor="middle" x="79.2793" y="-134.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="18.00" fill="#0000ff">$> ssh sshportal</text>
|
||||
</g>
|
||||
<!-- known_user_key -->
|
||||
<g id="node2" class="node">
|
||||
<title>known_user_key</title>
|
||||
<path fill="none" stroke="#ff8c00" d="M216.1104,-161C216.1104,-161 313.1514,-161 313.1514,-161 319.1514,-161 325.1514,-167 325.1514,-173 325.1514,-173 325.1514,-185 325.1514,-185 325.1514,-191 319.1514,-197 313.1514,-197 313.1514,-197 216.1104,-197 216.1104,-197 210.1104,-197 204.1104,-191 204.1104,-185 204.1104,-185 204.1104,-173 204.1104,-173 204.1104,-167 210.1104,-161 216.1104,-161"/>
|
||||
<text text-anchor="middle" x="264.6309" y="-174.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff8c00">known user key</text>
|
||||
</g>
|
||||
<!-- start->known_user_key -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>start->known_user_key</title>
|
||||
<path fill="none" stroke="#0000ff" d="M158.6917,-156.7092C173.8232,-159.8931 189.4365,-163.1783 203.8727,-166.2158"/>
|
||||
</g>
|
||||
<!-- unknown_user_key -->
|
||||
<g id="node3" class="node">
|
||||
<title>unknown_user_key</title>
|
||||
<path fill="none" stroke="#ff8c00" d="M207.5586,-69C207.5586,-69 321.7031,-69 321.7031,-69 327.7031,-69 333.7031,-75 333.7031,-81 333.7031,-81 333.7031,-93 333.7031,-93 333.7031,-99 327.7031,-105 321.7031,-105 321.7031,-105 207.5586,-105 207.5586,-105 201.5586,-105 195.5586,-99 195.5586,-93 195.5586,-93 195.5586,-81 195.5586,-81 195.5586,-75 201.5586,-69 207.5586,-69"/>
|
||||
<text text-anchor="middle" x="264.6309" y="-82.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff8c00">unknown user key</text>
|
||||
</g>
|
||||
<!-- start->unknown_user_key -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>start->unknown_user_key</title>
|
||||
<path fill="none" stroke="#0000ff" d="M142.2895,-121.9827C161.3902,-116.521 182.3703,-110.5218 201.4801,-105.0575"/>
|
||||
</g>
|
||||
<!-- acl_manager -->
|
||||
<g id="node5" class="node">
|
||||
<title>acl_manager</title>
|
||||
<path fill="none" stroke="#ff8c00" d="M608.9287,-173C608.9287,-173 691.7031,-173 691.7031,-173 697.7031,-173 703.7031,-179 703.7031,-185 703.7031,-185 703.7031,-197 703.7031,-197 703.7031,-203 697.7031,-209 691.7031,-209 691.7031,-209 608.9287,-209 608.9287,-209 602.9287,-209 596.9287,-203 596.9287,-197 596.9287,-197 596.9287,-185 596.9287,-185 596.9287,-179 602.9287,-173 608.9287,-173"/>
|
||||
<text text-anchor="middle" x="650.3159" y="-186.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff8c00">ACL manager</text>
|
||||
</g>
|
||||
<!-- known_user_key->acl_manager -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>known_user_key->acl_manager</title>
|
||||
<path fill="none" stroke="#ff8c00" d="M325.3184,-180.8882C399.9907,-183.2115 525.6007,-187.1197 596.8147,-189.3354"/>
|
||||
<text text-anchor="middle" x="463.3062" y="-190.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff8c00">user matches an existing host</text>
|
||||
</g>
|
||||
<!-- builtin_shell -->
|
||||
<g id="node6" class="node">
|
||||
<title>builtin_shell</title>
|
||||
<path fill="none" stroke="#006400" d="M874.6738,-223C874.6738,-223 944.46,-223 944.46,-223 950.46,-223 956.46,-229 956.46,-235 956.46,-235 956.46,-247 956.46,-247 956.46,-253 950.46,-259 944.46,-259 944.46,-259 874.6738,-259 874.6738,-259 868.6738,-259 862.6738,-253 862.6738,-247 862.6738,-247 862.6738,-235 862.6738,-235 862.6738,-229 868.6738,-223 874.6738,-223"/>
|
||||
<text text-anchor="middle" x="909.5669" y="-243.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">built-in</text>
|
||||
<text text-anchor="middle" x="909.5669" y="-229.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">config shell</text>
|
||||
</g>
|
||||
<!-- known_user_key->builtin_shell -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>known_user_key->builtin_shell</title>
|
||||
<path fill="none" stroke="#006400" d="M325.3695,-196.5059C340.0986,-200.1819 355.8759,-203.652 370.7031,-206 550.8024,-234.5204 768.2909,-239.9067 862.3934,-240.8487"/>
|
||||
<text text-anchor="middle" x="650.3159" y="-238.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#006400">user=admin</text>
|
||||
</g>
|
||||
<!-- err_and_exit -->
|
||||
<g id="node9" class="node">
|
||||
<title>err_and_exit</title>
|
||||
<path fill="none" stroke="#ff0000" d="M887.1152,-81C887.1152,-81 932.0186,-81 932.0186,-81 938.0186,-81 944.0186,-87 944.0186,-93 944.0186,-93 944.0186,-137 944.0186,-137 944.0186,-143 938.0186,-149 932.0186,-149 932.0186,-149 887.1152,-149 887.1152,-149 881.1152,-149 875.1152,-143 875.1152,-137 875.1152,-137 875.1152,-93 875.1152,-93 875.1152,-87 881.1152,-81 887.1152,-81"/>
|
||||
<text text-anchor="middle" x="909.5669" y="-117.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff0000">error</text>
|
||||
<text text-anchor="middle" x="909.5669" y="-103.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff0000">and exit</text>
|
||||
</g>
|
||||
<!-- known_user_key->err_and_exit -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>known_user_key->err_and_exit</title>
|
||||
<path fill="none" stroke="#ff0000" d="M325.3049,-172.979C457.9451,-159.8165 770.2119,-128.8288 874.7656,-118.4535"/>
|
||||
<text text-anchor="middle" x="650.3159" y="-148.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff0000">invalid user</text>
|
||||
</g>
|
||||
<!-- invite_manager -->
|
||||
<g id="node4" class="node">
|
||||
<title>invite_manager</title>
|
||||
<path fill="none" stroke="#ff8c00" d="M604.9092,-17C604.9092,-17 695.7227,-17 695.7227,-17 701.7227,-17 707.7227,-23 707.7227,-29 707.7227,-29 707.7227,-41 707.7227,-41 707.7227,-47 701.7227,-53 695.7227,-53 695.7227,-53 604.9092,-53 604.9092,-53 598.9092,-53 592.9092,-47 592.9092,-41 592.9092,-41 592.9092,-29 592.9092,-29 592.9092,-23 598.9092,-17 604.9092,-17"/>
|
||||
<text text-anchor="middle" x="650.3159" y="-30.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff8c00">invite manager</text>
|
||||
</g>
|
||||
<!-- unknown_user_key->invite_manager -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>unknown_user_key->invite_manager</title>
|
||||
<path fill="none" stroke="#ff8c00" d="M334.0291,-77.6434C407.9842,-67.6724 523.7263,-52.0674 592.789,-42.7561"/>
|
||||
<text text-anchor="middle" x="463.3062" y="-74.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff8c00">user=invite:<token></text>
|
||||
</g>
|
||||
<!-- unknown_user_key->err_and_exit -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>unknown_user_key->err_and_exit</title>
|
||||
<path fill="none" stroke="#ff0000" d="M333.7181,-89.2908C439.591,-92.8626 637.1209,-99.7853 707.7227,-104 724.1917,-104.9832 728.2588,-105.9333 744.7227,-107 789.6129,-109.9084 841.4427,-112.2584 874.8164,-113.641"/>
|
||||
<text text-anchor="middle" x="650.3159" y="-106.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff0000">any other user</text>
|
||||
</g>
|
||||
<!-- learn_key -->
|
||||
<g id="node8" class="node">
|
||||
<title>learn_key</title>
|
||||
<path fill="none" stroke="#006400" d="M884.3911,-17C884.3911,-17 934.7427,-17 934.7427,-17 940.7427,-17 946.7427,-23 946.7427,-29 946.7427,-29 946.7427,-41 946.7427,-41 946.7427,-47 940.7427,-53 934.7427,-53 934.7427,-53 884.3911,-53 884.3911,-53 878.3911,-53 872.3911,-47 872.3911,-41 872.3911,-41 872.3911,-29 872.3911,-29 872.3911,-23 878.3911,-17 884.3911,-17"/>
|
||||
<text text-anchor="middle" x="909.5669" y="-37.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">learn the</text>
|
||||
<text text-anchor="middle" x="909.5669" y="-23.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">pub key</text>
|
||||
</g>
|
||||
<!-- invite_manager->learn_key -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>invite_manager->learn_key</title>
|
||||
<path fill="none" stroke="#006400" d="M707.8521,-35C757.9748,-35 829.1828,-35 872.2155,-35"/>
|
||||
<text text-anchor="middle" x="785.1982" y="-37.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#006400">valid token</text>
|
||||
</g>
|
||||
<!-- invite_manager->err_and_exit -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>invite_manager->err_and_exit</title>
|
||||
<path fill="none" stroke="#ff0000" d="M707.8521,-52.7546C759.019,-68.5437 832.1589,-91.1133 874.868,-104.2926"/>
|
||||
<text text-anchor="middle" x="785.1982" y="-91.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff0000">invalid token</text>
|
||||
</g>
|
||||
<!-- ssh_proxy -->
|
||||
<g id="node7" class="node">
|
||||
<title>ssh_proxy</title>
|
||||
<path fill="none" stroke="#006400" d="M877.0117,-168C877.0117,-168 942.1221,-168 942.1221,-168 948.1221,-168 954.1221,-174 954.1221,-180 954.1221,-180 954.1221,-192 954.1221,-192 954.1221,-198 948.1221,-204 942.1221,-204 942.1221,-204 877.0117,-204 877.0117,-204 871.0117,-204 865.0117,-198 865.0117,-192 865.0117,-192 865.0117,-180 865.0117,-180 865.0117,-174 871.0117,-168 877.0117,-168"/>
|
||||
<text text-anchor="middle" x="909.5669" y="-188.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">SSH proxy</text>
|
||||
<text text-anchor="middle" x="909.5669" y="-174.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">Jump-Host</text>
|
||||
</g>
|
||||
<!-- acl_manager->ssh_proxy -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>acl_manager->ssh_proxy</title>
|
||||
<path fill="none" stroke="#006400" d="M704.0566,-192.4569C738.7694,-193.1138 784.9041,-193.4561 825.6738,-192 838.3694,-191.5466 852.1251,-190.7084 864.7541,-189.7993"/>
|
||||
<text text-anchor="middle" x="785.1982" y="-195.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#006400">authorized</text>
|
||||
</g>
|
||||
<!-- acl_manager->err_and_exit -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>acl_manager->err_and_exit</title>
|
||||
<path fill="none" stroke="#ff0000" d="M703.7163,-179.7682C743.1076,-170.9461 797.7781,-157.5732 844.6738,-142 854.6331,-138.6927 865.2245,-134.5604 874.8992,-130.5307"/>
|
||||
<text text-anchor="middle" x="785.1982" y="-172.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff0000">unauthorized</text>
|
||||
</g>
|
||||
<!-- host_1 -->
|
||||
<g id="node10" class="node">
|
||||
<title>host_1</title>
|
||||
<path fill="none" stroke="#0000ff" d="M1024.5425,-223C1024.5425,-223 1094.7456,-223 1094.7456,-223 1100.7456,-223 1106.7456,-229 1106.7456,-235 1106.7456,-235 1106.7456,-247 1106.7456,-247 1106.7456,-253 1100.7456,-259 1094.7456,-259 1094.7456,-259 1024.5425,-259 1024.5425,-259 1018.5425,-259 1012.5425,-253 1012.5425,-247 1012.5425,-247 1012.5425,-235 1012.5425,-235 1012.5425,-229 1018.5425,-223 1024.5425,-223"/>
|
||||
<text text-anchor="middle" x="1059.644" y="-236.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#0000ff">root@host1</text>
|
||||
</g>
|
||||
<!-- ssh_proxy->host_1 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>ssh_proxy->host_1</title>
|
||||
<path fill="none" stroke="#0000ff" d="M954.5012,-202.6151C964.678,-206.3678 975.4382,-210.3275 985.46,-214 994.2108,-217.2067 1003.5469,-220.6149 1012.54,-223.8913"/>
|
||||
</g>
|
||||
<!-- host_2 -->
|
||||
<g id="node11" class="node">
|
||||
<title>host_2</title>
|
||||
<path fill="none" stroke="#0000ff" d="M1005.46,-168C1005.46,-168 1113.8281,-168 1113.8281,-168 1119.8281,-168 1125.8281,-174 1125.8281,-180 1125.8281,-180 1125.8281,-192 1125.8281,-192 1125.8281,-198 1119.8281,-204 1113.8281,-204 1113.8281,-204 1005.46,-204 1005.46,-204 999.46,-204 993.46,-198 993.46,-192 993.46,-192 993.46,-180 993.46,-180 993.46,-174 999.46,-168 1005.46,-168"/>
|
||||
<text text-anchor="middle" x="1059.644" y="-181.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#0000ff">user@host2:2222</text>
|
||||
</g>
|
||||
<!-- ssh_proxy->host_2 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>ssh_proxy->host_2</title>
|
||||
<path fill="none" stroke="#0000ff" d="M954.1887,-186C966.458,-186 980.0332,-186 993.2463,-186"/>
|
||||
</g>
|
||||
<!-- host_3 -->
|
||||
<g id="node12" class="node">
|
||||
<title>host_3</title>
|
||||
<path fill="none" stroke="#0000ff" d="M1006.6392,-113C1006.6392,-113 1112.6489,-113 1112.6489,-113 1118.6489,-113 1124.6489,-119 1124.6489,-125 1124.6489,-125 1124.6489,-137 1124.6489,-137 1124.6489,-143 1118.6489,-149 1112.6489,-149 1112.6489,-149 1006.6392,-149 1006.6392,-149 1000.6392,-149 994.6392,-143 994.6392,-137 994.6392,-137 994.6392,-125 994.6392,-125 994.6392,-119 1000.6392,-113 1006.6392,-113"/>
|
||||
<text text-anchor="middle" x="1059.644" y="-126.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#0000ff">root@host3:1234</text>
|
||||
</g>
|
||||
<!-- ssh_proxy->host_3 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>ssh_proxy->host_3</title>
|
||||
<path fill="none" stroke="#0000ff" d="M954.1887,-169.6471C971.9014,-163.1558 992.3359,-155.667 1010.4731,-149.0201"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
36
.assets/overview.dot
Normal file
@@ -0,0 +1,36 @@
|
||||
graph {
|
||||
rankdir=LR;
|
||||
node[shape=box,style=rounded,style=rounded,fillcolor=gray];
|
||||
|
||||
|
||||
subgraph cluster_sshportal {
|
||||
sshportal[penwidth=3.0,color=brown,fontcolor=brown,fontsize=20];
|
||||
shell[label="built-in\nadmin shell",color=orange,fontcolor=orange];
|
||||
db[color=gray,fontcolor=gray,shape=circle];
|
||||
{ rank=same; db; sshportal; shell }
|
||||
}
|
||||
|
||||
{
|
||||
node[color="green"];
|
||||
host1; host2; host3; hostN;
|
||||
}
|
||||
|
||||
{
|
||||
node[color="blue"];
|
||||
user1; user2; user3; userN;
|
||||
}
|
||||
|
||||
{
|
||||
edge[penwidth=3.0];
|
||||
user1 -- sshportal -- host1[color=red];
|
||||
user2 -- sshportal -- host2[color=blue];
|
||||
user3 -- sshportal -- host1[color=purple];
|
||||
user2 -- sshportal -- host3[color=green];
|
||||
user2 -- sshportal -- shell[color=orange,constraint=false];
|
||||
}
|
||||
|
||||
userN -- sshportal[style=dotted];
|
||||
sshportal -- hostN[style=dotted];
|
||||
sshportal -- db[style=dotted,color=grey];
|
||||
|
||||
}
|
||||
BIN
.assets/overview.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
149
.assets/overview.svg
Normal file
@@ -0,0 +1,149 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: %3 Pages: 1 -->
|
||||
<svg width="276pt" height="224pt"
|
||||
viewBox="0.00 0.00 276.22 224.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 220)">
|
||||
<title>%3</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-220 272.2168,-220 272.2168,4 -4,4"/>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_sshportal</title>
|
||||
<polygon fill="none" stroke="#000000" points="82,-46 82,-208 186.2168,-208 186.2168,-46 82,-46"/>
|
||||
</g>
|
||||
<!-- sshportal -->
|
||||
<g id="node1" class="node">
|
||||
<title>sshportal</title>
|
||||
<path fill="none" stroke="#a52a2a" stroke-width="3" d="M166.3255,-144C166.3255,-144 101.8913,-144 101.8913,-144 95.8913,-144 89.8913,-138 89.8913,-132 89.8913,-132 89.8913,-120 89.8913,-120 89.8913,-114 95.8913,-108 101.8913,-108 101.8913,-108 166.3255,-108 166.3255,-108 172.3255,-108 178.3255,-114 178.3255,-120 178.3255,-120 178.3255,-132 178.3255,-132 178.3255,-138 172.3255,-144 166.3255,-144"/>
|
||||
<text text-anchor="middle" x="134.1084" y="-120" font-family="Times,serif" font-size="20.00" fill="#a52a2a">sshportal</text>
|
||||
</g>
|
||||
<!-- shell -->
|
||||
<g id="node2" class="node">
|
||||
<title>shell</title>
|
||||
<path fill="none" stroke="#ffa500" d="M162.543,-90C162.543,-90 105.6738,-90 105.6738,-90 99.6738,-90 93.6738,-84 93.6738,-78 93.6738,-78 93.6738,-66 93.6738,-66 93.6738,-60 99.6738,-54 105.6738,-54 105.6738,-54 162.543,-54 162.543,-54 168.543,-54 174.543,-60 174.543,-66 174.543,-66 174.543,-78 174.543,-78 174.543,-84 168.543,-90 162.543,-90"/>
|
||||
<text text-anchor="middle" x="134.1084" y="-74.8" font-family="Times,serif" font-size="14.00" fill="#ffa500">built-in</text>
|
||||
<text text-anchor="middle" x="134.1084" y="-60.8" font-family="Times,serif" font-size="14.00" fill="#ffa500">admin shell</text>
|
||||
</g>
|
||||
<!-- sshportal--shell -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>sshportal--shell</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-width="3" d="M134.1084,-107.7902C134.1084,-101.907 134.1084,-96.0238 134.1084,-90.1406"/>
|
||||
</g>
|
||||
<!-- db -->
|
||||
<g id="node3" class="node">
|
||||
<title>db</title>
|
||||
<ellipse fill="none" stroke="#c0c0c0" cx="134.1084" cy="-181" rx="18.9007" ry="18.9007"/>
|
||||
<text text-anchor="middle" x="134.1084" y="-176.8" font-family="Times,serif" font-size="14.00" fill="#c0c0c0">db</text>
|
||||
</g>
|
||||
<!-- sshportal--db -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>sshportal--db</title>
|
||||
<path fill="none" stroke="#c0c0c0" stroke-dasharray="1,5" d="M134.1084,-144.0469C134.1084,-150.0133 134.1084,-155.9797 134.1084,-161.946"/>
|
||||
</g>
|
||||
<!-- host1 -->
|
||||
<g id="node4" class="node">
|
||||
<title>host1</title>
|
||||
<path fill="none" stroke="#00ff00" d="M256.2168,-198C256.2168,-198 226.2168,-198 226.2168,-198 220.2168,-198 214.2168,-192 214.2168,-186 214.2168,-186 214.2168,-174 214.2168,-174 214.2168,-168 220.2168,-162 226.2168,-162 226.2168,-162 256.2168,-162 256.2168,-162 262.2168,-162 268.2168,-168 268.2168,-174 268.2168,-174 268.2168,-186 268.2168,-186 268.2168,-192 262.2168,-198 256.2168,-198"/>
|
||||
<text text-anchor="middle" x="241.2168" y="-175.8" font-family="Times,serif" font-size="14.00" fill="#000000">host1</text>
|
||||
</g>
|
||||
<!-- sshportal--host1 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>sshportal--host1</title>
|
||||
<path fill="none" stroke="#ff0000" stroke-width="3" d="M178.2919,-141.6183C191.4305,-147.98 205.3457,-155.29 216.7405,-161.8863"/>
|
||||
</g>
|
||||
<!-- sshportal--host1 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>sshportal--host1</title>
|
||||
<path fill="none" stroke="#a020f0" stroke-width="3" d="M158.413,-144.0143C174.9543,-153.6007 196.8661,-164.7159 213.9941,-172.2404"/>
|
||||
</g>
|
||||
<!-- host2 -->
|
||||
<g id="node5" class="node">
|
||||
<title>host2</title>
|
||||
<path fill="none" stroke="#00ff00" d="M256.2168,-144C256.2168,-144 226.2168,-144 226.2168,-144 220.2168,-144 214.2168,-138 214.2168,-132 214.2168,-132 214.2168,-120 214.2168,-120 214.2168,-114 220.2168,-108 226.2168,-108 226.2168,-108 256.2168,-108 256.2168,-108 262.2168,-108 268.2168,-114 268.2168,-120 268.2168,-120 268.2168,-132 268.2168,-132 268.2168,-138 262.2168,-144 256.2168,-144"/>
|
||||
<text text-anchor="middle" x="241.2168" y="-121.8" font-family="Times,serif" font-size="14.00" fill="#000000">host2</text>
|
||||
</g>
|
||||
<!-- sshportal--host2 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>sshportal--host2</title>
|
||||
<path fill="none" stroke="#0000ff" stroke-width="3" d="M178.2919,-126C190.3932,-126 203.1534,-126 213.9962,-126"/>
|
||||
</g>
|
||||
<!-- host3 -->
|
||||
<g id="node6" class="node">
|
||||
<title>host3</title>
|
||||
<path fill="none" stroke="#00ff00" d="M256.2168,-90C256.2168,-90 226.2168,-90 226.2168,-90 220.2168,-90 214.2168,-84 214.2168,-78 214.2168,-78 214.2168,-66 214.2168,-66 214.2168,-60 220.2168,-54 226.2168,-54 226.2168,-54 256.2168,-54 256.2168,-54 262.2168,-54 268.2168,-60 268.2168,-66 268.2168,-66 268.2168,-78 268.2168,-78 268.2168,-84 262.2168,-90 256.2168,-90"/>
|
||||
<text text-anchor="middle" x="241.2168" y="-67.8" font-family="Times,serif" font-size="14.00" fill="#000000">host3</text>
|
||||
</g>
|
||||
<!-- sshportal--host3 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>sshportal--host3</title>
|
||||
<path fill="none" stroke="#00ff00" stroke-width="3" d="M170.0719,-107.8686C184.4145,-100.6376 200.6507,-92.4519 213.9876,-85.728"/>
|
||||
</g>
|
||||
<!-- hostN -->
|
||||
<g id="node7" class="node">
|
||||
<title>hostN</title>
|
||||
<path fill="none" stroke="#00ff00" d="M256.2168,-36C256.2168,-36 226.2168,-36 226.2168,-36 220.2168,-36 214.2168,-30 214.2168,-24 214.2168,-24 214.2168,-12 214.2168,-12 214.2168,-6 220.2168,0 226.2168,0 226.2168,0 256.2168,0 256.2168,0 262.2168,0 268.2168,-6 268.2168,-12 268.2168,-12 268.2168,-24 268.2168,-24 268.2168,-30 262.2168,-36 256.2168,-36"/>
|
||||
<text text-anchor="middle" x="241.2168" y="-13.8" font-family="Times,serif" font-size="14.00" fill="#000000">hostN</text>
|
||||
</g>
|
||||
<!-- sshportal--hostN -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>sshportal--hostN</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M175.0827,-107.9914C179.0963,-105.3082 182.9022,-102.3137 186.2168,-99 205.3358,-79.8865 198.2486,-66.8147 214.2168,-45 216.4095,-42.0045 218.9349,-39.0275 221.5425,-36.2043"/>
|
||||
</g>
|
||||
<!-- user1 -->
|
||||
<g id="node8" class="node">
|
||||
<title>user1</title>
|
||||
<path fill="none" stroke="#0000ff" d="M42,-198C42,-198 12,-198 12,-198 6,-198 0,-192 0,-186 0,-186 0,-174 0,-174 0,-168 6,-162 12,-162 12,-162 42,-162 42,-162 48,-162 54,-168 54,-174 54,-174 54,-186 54,-186 54,-192 48,-198 42,-198"/>
|
||||
<text text-anchor="middle" x="27" y="-175.8" font-family="Times,serif" font-size="14.00" fill="#000000">user1</text>
|
||||
</g>
|
||||
<!-- user1--sshportal -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>user1--sshportal</title>
|
||||
<path fill="none" stroke="#ff0000" stroke-width="3" d="M54.028,-166.3735C67.4141,-159.6248 83.7582,-151.3846 98.1822,-144.1126"/>
|
||||
</g>
|
||||
<!-- user2 -->
|
||||
<g id="node9" class="node">
|
||||
<title>user2</title>
|
||||
<path fill="none" stroke="#0000ff" d="M42,-144C42,-144 12,-144 12,-144 6,-144 0,-138 0,-132 0,-132 0,-120 0,-120 0,-114 6,-108 12,-108 12,-108 42,-108 42,-108 48,-108 54,-114 54,-120 54,-120 54,-132 54,-132 54,-138 48,-144 42,-144"/>
|
||||
<text text-anchor="middle" x="27" y="-121.8" font-family="Times,serif" font-size="14.00" fill="#000000">user2</text>
|
||||
</g>
|
||||
<!-- user2--sshportal -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>user2--sshportal</title>
|
||||
<path fill="none" stroke="#0000ff" stroke-width="3" d="M54.028,-114.0952C64.8112,-112.6824 77.514,-112.208 89.5863,-112.6721"/>
|
||||
</g>
|
||||
<!-- user2--sshportal -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>user2--sshportal</title>
|
||||
<path fill="none" stroke="#00ff00" stroke-width="3" d="M54.028,-126C64.8112,-126 77.514,-126 89.5863,-126"/>
|
||||
</g>
|
||||
<!-- user2--sshportal -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>user2--sshportal</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-width="3" d="M54.028,-137.9048C64.8112,-139.3176 77.514,-139.792 89.5863,-139.3279"/>
|
||||
</g>
|
||||
<!-- user3 -->
|
||||
<g id="node10" class="node">
|
||||
<title>user3</title>
|
||||
<path fill="none" stroke="#0000ff" d="M42,-90C42,-90 12,-90 12,-90 6,-90 0,-84 0,-78 0,-78 0,-66 0,-66 0,-60 6,-54 12,-54 12,-54 42,-54 42,-54 48,-54 54,-60 54,-66 54,-66 54,-78 54,-78 54,-84 48,-90 42,-90"/>
|
||||
<text text-anchor="middle" x="27" y="-67.8" font-family="Times,serif" font-size="14.00" fill="#000000">user3</text>
|
||||
</g>
|
||||
<!-- user3--sshportal -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>user3--sshportal</title>
|
||||
<path fill="none" stroke="#a020f0" stroke-width="3" d="M54.028,-85.6265C67.4141,-92.3752 83.7582,-100.6154 98.1822,-107.8874"/>
|
||||
</g>
|
||||
<!-- userN -->
|
||||
<g id="node11" class="node">
|
||||
<title>userN</title>
|
||||
<path fill="none" stroke="#0000ff" d="M42,-36C42,-36 12,-36 12,-36 6,-36 0,-30 0,-24 0,-24 0,-12 0,-12 0,-6 6,0 12,0 12,0 42,0 42,0 48,0 54,-6 54,-12 54,-12 54,-24 54,-24 54,-30 48,-36 42,-36"/>
|
||||
<text text-anchor="middle" x="27" y="-13.8" font-family="Times,serif" font-size="14.00" fill="#000000">userN</text>
|
||||
</g>
|
||||
<!-- userN--sshportal -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>userN--sshportal</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M46.6743,-36.2043C49.2819,-39.0275 51.8073,-42.0045 54,-45 69.9682,-66.8147 62.881,-79.8865 82,-99 85.3146,-102.3137 89.1205,-105.3082 93.1341,-107.9914"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.2 KiB |
BIN
.assets/server.gif
Normal file
|
After Width: | Height: | Size: 43 KiB |
390
.assets/sql-schema.svg
Normal file
@@ -0,0 +1,390 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: Database Structure Pages: 1 -->
|
||||
<svg width="1498pt" height="1073pt"
|
||||
viewBox="0.00 0.00 1498.00 1073.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1069)">
|
||||
<title>Database Structure</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1069 1494,-1069 1494,4 -4,4"/>
|
||||
<text text-anchor="middle" x="745" y="-1049.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#000000">ER Diagram: db</text>
|
||||
<!-- acls -->
|
||||
<g id="node1" class="node">
|
||||
<title>acls</title>
|
||||
<polygon fill="#dddddd" stroke="transparent" points="700,-556 700,-576 828,-576 828,-556 700,-556"/>
|
||||
<polygon fill="none" stroke="#000000" points="700,-556 700,-576 828,-576 828,-556 700,-556"/>
|
||||
<text text-anchor="start" x="753.1172" y="-561.8" font-family="Times,serif" font-size="14.00" fill="#000000">acls</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-536 700,-556 828,-556 828,-536 700,-536"/>
|
||||
<text text-anchor="start" x="748.5562" y="-542.8" font-family="Times,serif" font-size="14.00" fill="#000000">id:</text>
|
||||
<text text-anchor="start" x="763.3354" y="-542.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-516 700,-536 828,-536 828,-516 700,-516"/>
|
||||
<text text-anchor="start" x="704.501" y="-522.8" font-family="Times,serif" font-size="14.00" fill="#000000">created_at:</text>
|
||||
<text text-anchor="start" x="765.9014" y="-522.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-496 700,-516 828,-516 828,-496 700,-496"/>
|
||||
<text text-anchor="start" x="702.5459" y="-502.8" font-family="Times,serif" font-size="14.00" fill="#000000">updated_at:</text>
|
||||
<text text-anchor="start" x="767.8564" y="-502.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-476 700,-496 828,-496 828,-476 700,-476"/>
|
||||
<text text-anchor="start" x="704.4941" y="-482.8" font-family="Times,serif" font-size="14.00" fill="#000000">deleted_at:</text>
|
||||
<text text-anchor="start" x="765.9082" y="-482.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-456 700,-476 828,-476 828,-456 700,-456"/>
|
||||
<text text-anchor="start" x="703.3721" y="-462.8" font-family="Times,serif" font-size="14.00" fill="#000000">host_pattern:</text>
|
||||
<text text-anchor="start" x="776.4688" y="-462.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-436 700,-456 828,-456 828,-436 700,-436"/>
|
||||
<text text-anchor="start" x="720.8721" y="-442.8" font-family="Times,serif" font-size="14.00" fill="#000000">action:</text>
|
||||
<text text-anchor="start" x="758.9688" y="-442.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-416 700,-436 828,-436 828,-416 700,-416"/>
|
||||
<text text-anchor="start" x="734.9492" y="-422.8" font-family="Times,serif" font-size="14.00" fill="#000000">weight:</text>
|
||||
<text text-anchor="start" x="776.9424" y="-422.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-396 700,-416 828,-416 828,-396 700,-396"/>
|
||||
<text text-anchor="start" x="711.9272" y="-402.8" font-family="Times,serif" font-size="14.00" fill="#000000">comment:</text>
|
||||
<text text-anchor="start" x="767.9136" y="-402.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
</g>
|
||||
<!-- host_group_acls -->
|
||||
<g id="node2" class="node">
|
||||
<title>host_group_acls</title>
|
||||
<polygon fill="#dddddd" stroke="transparent" points="137,-673 137,-693 243,-693 243,-673 137,-673"/>
|
||||
<polygon fill="none" stroke="#000000" points="137,-673 137,-693 243,-693 243,-673 137,-673"/>
|
||||
<text text-anchor="start" x="144.1172" y="-678.8" font-family="Times,serif" font-size="14.00" fill="#000000">host_group_acls</text>
|
||||
<polygon fill="none" stroke="#000000" points="137,-653 137,-673 243,-673 243,-653 137,-653"/>
|
||||
<text text-anchor="start" x="139.5562" y="-659.8" font-family="Times,serif" font-size="14.00" fill="#000000">host_group_id:</text>
|
||||
<text text-anchor="start" x="224.3354" y="-659.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="137,-633 137,-653 243,-653 243,-633 137,-633"/>
|
||||
<text text-anchor="start" x="162.8975" y="-639.8" font-family="Times,serif" font-size="14.00" fill="#000000">acl_id:</text>
|
||||
<text text-anchor="start" x="200.9941" y="-639.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
</g>
|
||||
<!-- host_group_acls->acls -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host_group_acls:acl_id->acls:id</title>
|
||||
<path fill="none" stroke="#444444" stroke-dasharray="5,2" d="M243,-643C447.1889,-643 490.9328,-549.194 689.7944,-546.0793"/>
|
||||
<polygon fill="#444444" stroke="#444444" points="700.0272,-549.4999 690.0003,-546.0778 699.9728,-542.5001 700.0272,-549.4999"/>
|
||||
</g>
|
||||
<!-- host_groups -->
|
||||
<g id="node3" class="node">
|
||||
<title>host_groups</title>
|
||||
<polygon fill="#dddddd" stroke="transparent" points="700,-722 700,-742 828,-742 828,-722 700,-722"/>
|
||||
<polygon fill="none" stroke="#000000" points="700,-722 700,-742 828,-742 828,-722 700,-722"/>
|
||||
<text text-anchor="start" x="729.7759" y="-727.8" font-family="Times,serif" font-size="14.00" fill="#000000">host_groups</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-702 700,-722 828,-722 828,-702 700,-702"/>
|
||||
<text text-anchor="start" x="748.5562" y="-708.8" font-family="Times,serif" font-size="14.00" fill="#000000">id:</text>
|
||||
<text text-anchor="start" x="763.3354" y="-708.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-682 700,-702 828,-702 828,-682 700,-682"/>
|
||||
<text text-anchor="start" x="704.501" y="-688.8" font-family="Times,serif" font-size="14.00" fill="#000000">created_at:</text>
|
||||
<text text-anchor="start" x="765.9014" y="-688.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-662 700,-682 828,-682 828,-662 700,-662"/>
|
||||
<text text-anchor="start" x="702.5459" y="-668.8" font-family="Times,serif" font-size="14.00" fill="#000000">updated_at:</text>
|
||||
<text text-anchor="start" x="767.8564" y="-668.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-642 700,-662 828,-662 828,-642 700,-642"/>
|
||||
<text text-anchor="start" x="704.4941" y="-648.8" font-family="Times,serif" font-size="14.00" fill="#000000">deleted_at:</text>
|
||||
<text text-anchor="start" x="765.9082" y="-648.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-622 700,-642 828,-642 828,-622 700,-622"/>
|
||||
<text text-anchor="start" x="722.8169" y="-628.8" font-family="Times,serif" font-size="14.00" fill="#000000">name:</text>
|
||||
<text text-anchor="start" x="757.0239" y="-628.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-602 700,-622 828,-622 828,-602 700,-602"/>
|
||||
<text text-anchor="start" x="711.9272" y="-608.8" font-family="Times,serif" font-size="14.00" fill="#000000">comment:</text>
|
||||
<text text-anchor="start" x="767.9136" y="-608.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
</g>
|
||||
<!-- host_group_acls->host_groups -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host_group_acls:host_group_id->host_groups:id</title>
|
||||
<path fill="none" stroke="#444444" stroke-dasharray="5,2" d="M243,-663C443.884,-663 494.0731,-710.3866 689.9535,-711.9599"/>
|
||||
<polygon fill="#444444" stroke="#444444" points="699.986,-715.5 690.0001,-711.96 700.014,-708.5 699.986,-715.5"/>
|
||||
</g>
|
||||
<!-- host_host_groups -->
|
||||
<g id="node4" class="node">
|
||||
<title>host_host_groups</title>
|
||||
<polygon fill="#dddddd" stroke="transparent" points="137,-787 137,-807 243,-807 243,-787 137,-787"/>
|
||||
<polygon fill="none" stroke="#000000" points="137,-787 137,-807 243,-807 243,-787 137,-787"/>
|
||||
<text text-anchor="start" x="140.6069" y="-792.8" font-family="Times,serif" font-size="14.00" fill="#000000">host_host_groups</text>
|
||||
<polygon fill="none" stroke="#000000" points="137,-767 137,-787 243,-787 243,-767 137,-767"/>
|
||||
<text text-anchor="start" x="159.3872" y="-773.8" font-family="Times,serif" font-size="14.00" fill="#000000">host_id:</text>
|
||||
<text text-anchor="start" x="204.5044" y="-773.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="137,-747 137,-767 243,-767 243,-747 137,-747"/>
|
||||
<text text-anchor="start" x="139.5562" y="-753.8" font-family="Times,serif" font-size="14.00" fill="#000000">host_group_id:</text>
|
||||
<text text-anchor="start" x="224.3354" y="-753.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
</g>
|
||||
<!-- host_host_groups->host_groups -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>host_host_groups:host_group_id->host_groups:id</title>
|
||||
<path fill="none" stroke="#444444" stroke-dasharray="5,2" d="M243,-757C443.7051,-757 494.243,-713.4817 689.9622,-712.0368"/>
|
||||
<polygon fill="#444444" stroke="#444444" points="700.0129,-715.5 690.0001,-712.0368 699.9871,-708.5 700.0129,-715.5"/>
|
||||
</g>
|
||||
<!-- hosts -->
|
||||
<g id="node5" class="node">
|
||||
<title>hosts</title>
|
||||
<polygon fill="#dddddd" stroke="transparent" points="700,-1008 700,-1028 828,-1028 828,-1008 700,-1008"/>
|
||||
<polygon fill="none" stroke="#000000" points="700,-1008 700,-1028 828,-1028 828,-1008 700,-1008"/>
|
||||
<text text-anchor="start" x="749.6069" y="-1013.8" font-family="Times,serif" font-size="14.00" fill="#000000">hosts</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-988 700,-1008 828,-1008 828,-988 700,-988"/>
|
||||
<text text-anchor="start" x="748.5562" y="-994.8" font-family="Times,serif" font-size="14.00" fill="#000000">id:</text>
|
||||
<text text-anchor="start" x="763.3354" y="-994.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-968 700,-988 828,-988 828,-968 700,-968"/>
|
||||
<text text-anchor="start" x="704.501" y="-974.8" font-family="Times,serif" font-size="14.00" fill="#000000">created_at:</text>
|
||||
<text text-anchor="start" x="765.9014" y="-974.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-948 700,-968 828,-968 828,-948 700,-948"/>
|
||||
<text text-anchor="start" x="702.5459" y="-954.8" font-family="Times,serif" font-size="14.00" fill="#000000">updated_at:</text>
|
||||
<text text-anchor="start" x="767.8564" y="-954.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-928 700,-948 828,-948 828,-928 700,-928"/>
|
||||
<text text-anchor="start" x="704.4941" y="-934.8" font-family="Times,serif" font-size="14.00" fill="#000000">deleted_at:</text>
|
||||
<text text-anchor="start" x="765.9082" y="-934.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-908 700,-928 828,-928 828,-908 700,-908"/>
|
||||
<text text-anchor="start" x="722.8169" y="-914.8" font-family="Times,serif" font-size="14.00" fill="#000000">name:</text>
|
||||
<text text-anchor="start" x="757.0239" y="-914.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-888 700,-908 828,-908 828,-888 700,-888"/>
|
||||
<text text-anchor="start" x="725.5376" y="-894.8" font-family="Times,serif" font-size="14.00" fill="#000000">addr:</text>
|
||||
<text text-anchor="start" x="754.3032" y="-894.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-868 700,-888 828,-888 828,-868 700,-868"/>
|
||||
<text text-anchor="start" x="726.3135" y="-874.8" font-family="Times,serif" font-size="14.00" fill="#000000">user:</text>
|
||||
<text text-anchor="start" x="753.5273" y="-874.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-848 700,-868 828,-868 828,-848 700,-848"/>
|
||||
<text text-anchor="start" x="711.5342" y="-854.8" font-family="Times,serif" font-size="14.00" fill="#000000">password:</text>
|
||||
<text text-anchor="start" x="768.3066" y="-854.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-828 700,-848 828,-848 828,-828 700,-828"/>
|
||||
<text text-anchor="start" x="722.501" y="-834.8" font-family="Times,serif" font-size="14.00" fill="#000000">ssh_key_id:</text>
|
||||
<text text-anchor="start" x="789.3906" y="-834.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-808 700,-828 828,-828 828,-808 700,-808"/>
|
||||
<text text-anchor="start" x="708.4238" y="-814.8" font-family="Times,serif" font-size="14.00" fill="#000000">fingerprint:</text>
|
||||
<text text-anchor="start" x="771.417" y="-814.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-788 700,-808 828,-808 828,-788 700,-788"/>
|
||||
<text text-anchor="start" x="711.9272" y="-794.8" font-family="Times,serif" font-size="14.00" fill="#000000">comment:</text>
|
||||
<text text-anchor="start" x="767.9136" y="-794.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-768 700,-788 828,-788 828,-768 700,-768"/>
|
||||
<text text-anchor="start" x="708.3394" y="-774.8" font-family="Times,serif" font-size="14.00" fill="#000000">host_key:</text>
|
||||
<text text-anchor="start" x="762.7808" y="-774.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARBINARY</text>
|
||||
</g>
|
||||
<!-- host_host_groups->hosts -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>host_host_groups:host_id->hosts:id</title>
|
||||
<path fill="none" stroke="#444444" stroke-dasharray="5,2" d="M243,-777C465.1991,-777 474.1608,-991.3602 689.9044,-997.8496"/>
|
||||
<polygon fill="#444444" stroke="#444444" points="699.9478,-1001.4996 690.0011,-997.851 700.0522,-994.5004 699.9478,-1001.4996"/>
|
||||
</g>
|
||||
<!-- ssh_keys -->
|
||||
<g id="node8" class="node">
|
||||
<title>ssh_keys</title>
|
||||
<polygon fill="#dddddd" stroke="transparent" points="1255,-848 1255,-868 1383,-868 1383,-848 1255,-848"/>
|
||||
<polygon fill="none" stroke="#000000" points="1255,-848 1255,-868 1383,-868 1383,-848 1255,-848"/>
|
||||
<text text-anchor="start" x="1293.7207" y="-853.8" font-family="Times,serif" font-size="14.00" fill="#000000">ssh_keys</text>
|
||||
<polygon fill="none" stroke="#000000" points="1255,-828 1255,-848 1383,-848 1383,-828 1255,-828"/>
|
||||
<text text-anchor="start" x="1303.5562" y="-834.8" font-family="Times,serif" font-size="14.00" fill="#000000">id:</text>
|
||||
<text text-anchor="start" x="1318.3354" y="-834.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="1255,-808 1255,-828 1383,-828 1383,-808 1255,-808"/>
|
||||
<text text-anchor="start" x="1259.501" y="-814.8" font-family="Times,serif" font-size="14.00" fill="#000000">created_at:</text>
|
||||
<text text-anchor="start" x="1320.9014" y="-814.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="1255,-788 1255,-808 1383,-808 1383,-788 1255,-788"/>
|
||||
<text text-anchor="start" x="1257.5459" y="-794.8" font-family="Times,serif" font-size="14.00" fill="#000000">updated_at:</text>
|
||||
<text text-anchor="start" x="1322.8564" y="-794.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="1255,-768 1255,-788 1383,-788 1383,-768 1255,-768"/>
|
||||
<text text-anchor="start" x="1259.4941" y="-774.8" font-family="Times,serif" font-size="14.00" fill="#000000">deleted_at:</text>
|
||||
<text text-anchor="start" x="1320.9082" y="-774.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="1255,-748 1255,-768 1383,-768 1383,-748 1255,-748"/>
|
||||
<text text-anchor="start" x="1277.8169" y="-754.8" font-family="Times,serif" font-size="14.00" fill="#000000">name:</text>
|
||||
<text text-anchor="start" x="1312.0239" y="-754.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="1255,-728 1255,-748 1383,-748 1383,-728 1255,-728"/>
|
||||
<text text-anchor="start" x="1280.9238" y="-734.8" font-family="Times,serif" font-size="14.00" fill="#000000">type:</text>
|
||||
<text text-anchor="start" x="1308.917" y="-734.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="1255,-708 1255,-728 1383,-728 1383,-708 1255,-708"/>
|
||||
<text text-anchor="start" x="1291.5044" y="-714.8" font-family="Times,serif" font-size="14.00" fill="#000000">length:</text>
|
||||
<text text-anchor="start" x="1330.3872" y="-714.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="1255,-688 1255,-708 1383,-708 1383,-688 1255,-688"/>
|
||||
<text text-anchor="start" x="1263.4238" y="-694.8" font-family="Times,serif" font-size="14.00" fill="#000000">fingerprint:</text>
|
||||
<text text-anchor="start" x="1326.417" y="-694.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="1255,-668 1255,-688 1383,-688 1383,-668 1255,-668"/>
|
||||
<text text-anchor="start" x="1268.0928" y="-674.8" font-family="Times,serif" font-size="14.00" fill="#000000">priv_key:</text>
|
||||
<text text-anchor="start" x="1321.748" y="-674.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="1255,-648 1255,-668 1383,-668 1383,-648 1255,-648"/>
|
||||
<text text-anchor="start" x="1268.8687" y="-654.8" font-family="Times,serif" font-size="14.00" fill="#000000">pub_key:</text>
|
||||
<text text-anchor="start" x="1320.9722" y="-654.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="1255,-628 1255,-648 1383,-648 1383,-628 1255,-628"/>
|
||||
<text text-anchor="start" x="1266.9272" y="-634.8" font-family="Times,serif" font-size="14.00" fill="#000000">comment:</text>
|
||||
<text text-anchor="start" x="1322.9136" y="-634.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
</g>
|
||||
<!-- hosts->ssh_keys -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>hosts:ssh_key_id->ssh_keys:id</title>
|
||||
<path fill="none" stroke="#444444" stroke-dasharray="5,2" d="M828,-838C1014.3492,-838 1063.4615,-838 1244.8519,-838"/>
|
||||
<polygon fill="#444444" stroke="#444444" points="1255,-841.5 1245,-838.0001 1255,-834.5 1255,-841.5"/>
|
||||
</g>
|
||||
<!-- migrations -->
|
||||
<g id="node6" class="node">
|
||||
<title>migrations</title>
|
||||
<polygon fill="#dddddd" stroke="transparent" points="156,-853 156,-873 224,-873 224,-853 156,-853"/>
|
||||
<polygon fill="none" stroke="#000000" points="156,-853 156,-873 224,-873 224,-853 156,-853"/>
|
||||
<text text-anchor="start" x="160.0586" y="-858.8" font-family="Times,serif" font-size="14.00" fill="#000000">migrations</text>
|
||||
<polygon fill="none" stroke="#000000" points="156,-833 156,-853 224,-853 224,-833 156,-833"/>
|
||||
<text text-anchor="start" x="158.5308" y="-839.8" font-family="Times,serif" font-size="14.00" fill="#000000">id:</text>
|
||||
<text text-anchor="start" x="173.3101" y="-839.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
</g>
|
||||
<!-- settings -->
|
||||
<g id="node7" class="node">
|
||||
<title>settings</title>
|
||||
<polygon fill="#dddddd" stroke="transparent" points="126,-1019 126,-1039 254,-1039 254,-1019 126,-1019"/>
|
||||
<polygon fill="none" stroke="#000000" points="126,-1019 126,-1039 254,-1039 254,-1019 126,-1019"/>
|
||||
<text text-anchor="start" x="168.6104" y="-1024.8" font-family="Times,serif" font-size="14.00" fill="#000000">settings</text>
|
||||
<polygon fill="none" stroke="#000000" points="126,-999 126,-1019 254,-1019 254,-999 126,-999"/>
|
||||
<text text-anchor="start" x="174.5562" y="-1005.8" font-family="Times,serif" font-size="14.00" fill="#000000">id:</text>
|
||||
<text text-anchor="start" x="189.3354" y="-1005.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="126,-979 126,-999 254,-999 254,-979 126,-979"/>
|
||||
<text text-anchor="start" x="130.501" y="-985.8" font-family="Times,serif" font-size="14.00" fill="#000000">created_at:</text>
|
||||
<text text-anchor="start" x="191.9014" y="-985.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="126,-959 126,-979 254,-979 254,-959 126,-959"/>
|
||||
<text text-anchor="start" x="128.5459" y="-965.8" font-family="Times,serif" font-size="14.00" fill="#000000">updated_at:</text>
|
||||
<text text-anchor="start" x="193.8564" y="-965.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="126,-939 126,-959 254,-959 254,-939 126,-939"/>
|
||||
<text text-anchor="start" x="130.4941" y="-945.8" font-family="Times,serif" font-size="14.00" fill="#000000">deleted_at:</text>
|
||||
<text text-anchor="start" x="191.9082" y="-945.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="126,-919 126,-939 254,-939 254,-919 126,-919"/>
|
||||
<text text-anchor="start" x="148.8169" y="-925.8" font-family="Times,serif" font-size="14.00" fill="#000000">name:</text>
|
||||
<text text-anchor="start" x="183.0239" y="-925.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="126,-899 126,-919 254,-919 254,-899 126,-899"/>
|
||||
<text text-anchor="start" x="148.8169" y="-905.8" font-family="Times,serif" font-size="14.00" fill="#000000">value:</text>
|
||||
<text text-anchor="start" x="183.0239" y="-905.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
</g>
|
||||
<!-- user_group_acls -->
|
||||
<g id="node9" class="node">
|
||||
<title>user_group_acls</title>
|
||||
<polygon fill="#dddddd" stroke="transparent" points="137,-530 137,-550 243,-550 243,-530 137,-530"/>
|
||||
<polygon fill="none" stroke="#000000" points="137,-530 137,-550 243,-550 243,-530 137,-530"/>
|
||||
<text text-anchor="start" x="144.124" y="-535.8" font-family="Times,serif" font-size="14.00" fill="#000000">user_group_acls</text>
|
||||
<polygon fill="none" stroke="#000000" points="137,-510 137,-530 243,-530 243,-510 137,-510"/>
|
||||
<text text-anchor="start" x="139.563" y="-516.8" font-family="Times,serif" font-size="14.00" fill="#000000">user_group_id:</text>
|
||||
<text text-anchor="start" x="224.3286" y="-516.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="137,-490 137,-510 243,-510 243,-490 137,-490"/>
|
||||
<text text-anchor="start" x="162.8975" y="-496.8" font-family="Times,serif" font-size="14.00" fill="#000000">acl_id:</text>
|
||||
<text text-anchor="start" x="200.9941" y="-496.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
</g>
|
||||
<!-- user_group_acls->acls -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>user_group_acls:acl_id->acls:id</title>
|
||||
<path fill="none" stroke="#444444" stroke-dasharray="5,2" d="M243,-500C443.7485,-500 494.2019,-544.4853 689.9601,-545.9624"/>
|
||||
<polygon fill="#444444" stroke="#444444" points="699.9869,-549.5 690.0001,-545.9624 700.0131,-542.5 699.9869,-549.5"/>
|
||||
</g>
|
||||
<!-- user_groups -->
|
||||
<g id="node10" class="node">
|
||||
<title>user_groups</title>
|
||||
<polygon fill="#dddddd" stroke="transparent" points="700,-350 700,-370 828,-370 828,-350 700,-350"/>
|
||||
<polygon fill="none" stroke="#000000" points="700,-350 700,-370 828,-370 828,-350 700,-350"/>
|
||||
<text text-anchor="start" x="729.7827" y="-355.8" font-family="Times,serif" font-size="14.00" fill="#000000">user_groups</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-330 700,-350 828,-350 828,-330 700,-330"/>
|
||||
<text text-anchor="start" x="748.5562" y="-336.8" font-family="Times,serif" font-size="14.00" fill="#000000">id:</text>
|
||||
<text text-anchor="start" x="763.3354" y="-336.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-310 700,-330 828,-330 828,-310 700,-310"/>
|
||||
<text text-anchor="start" x="704.501" y="-316.8" font-family="Times,serif" font-size="14.00" fill="#000000">created_at:</text>
|
||||
<text text-anchor="start" x="765.9014" y="-316.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-290 700,-310 828,-310 828,-290 700,-290"/>
|
||||
<text text-anchor="start" x="702.5459" y="-296.8" font-family="Times,serif" font-size="14.00" fill="#000000">updated_at:</text>
|
||||
<text text-anchor="start" x="767.8564" y="-296.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-270 700,-290 828,-290 828,-270 700,-270"/>
|
||||
<text text-anchor="start" x="704.4941" y="-276.8" font-family="Times,serif" font-size="14.00" fill="#000000">deleted_at:</text>
|
||||
<text text-anchor="start" x="765.9082" y="-276.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-250 700,-270 828,-270 828,-250 700,-250"/>
|
||||
<text text-anchor="start" x="722.8169" y="-256.8" font-family="Times,serif" font-size="14.00" fill="#000000">name:</text>
|
||||
<text text-anchor="start" x="757.0239" y="-256.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-230 700,-250 828,-250 828,-230 700,-230"/>
|
||||
<text text-anchor="start" x="711.9272" y="-236.8" font-family="Times,serif" font-size="14.00" fill="#000000">comment:</text>
|
||||
<text text-anchor="start" x="767.9136" y="-236.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
</g>
|
||||
<!-- user_group_acls->user_groups -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>user_group_acls:user_group_id->user_groups:id</title>
|
||||
<path fill="none" stroke="#444444" stroke-dasharray="5,2" d="M243,-520C457.8873,-520 481.0158,-345.5811 689.9107,-340.1305"/>
|
||||
<polygon fill="#444444" stroke="#444444" points="700.0453,-343.4997 690.0008,-340.1294 699.9547,-336.5003 700.0453,-343.4997"/>
|
||||
</g>
|
||||
<!-- user_keys -->
|
||||
<g id="node11" class="node">
|
||||
<title>user_keys</title>
|
||||
<polygon fill="#dddddd" stroke="transparent" points="118,-284 118,-304 262,-304 262,-284 118,-284"/>
|
||||
<polygon fill="none" stroke="#000000" points="118,-284 118,-304 262,-304 262,-284 118,-284"/>
|
||||
<text text-anchor="start" x="162.0068" y="-289.8" font-family="Times,serif" font-size="14.00" fill="#000000">user_keys</text>
|
||||
<polygon fill="none" stroke="#000000" points="118,-264 118,-284 262,-284 262,-264 118,-264"/>
|
||||
<text text-anchor="start" x="174.5562" y="-270.8" font-family="Times,serif" font-size="14.00" fill="#000000">id:</text>
|
||||
<text text-anchor="start" x="189.3354" y="-270.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="118,-244 118,-264 262,-264 262,-244 118,-244"/>
|
||||
<text text-anchor="start" x="130.501" y="-250.8" font-family="Times,serif" font-size="14.00" fill="#000000">created_at:</text>
|
||||
<text text-anchor="start" x="191.9014" y="-250.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="118,-224 118,-244 262,-244 262,-224 118,-224"/>
|
||||
<text text-anchor="start" x="128.5459" y="-230.8" font-family="Times,serif" font-size="14.00" fill="#000000">updated_at:</text>
|
||||
<text text-anchor="start" x="193.8564" y="-230.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="118,-204 118,-224 262,-224 262,-204 118,-204"/>
|
||||
<text text-anchor="start" x="130.4941" y="-210.8" font-family="Times,serif" font-size="14.00" fill="#000000">deleted_at:</text>
|
||||
<text text-anchor="start" x="191.9082" y="-210.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="118,-184 118,-204 262,-204 262,-184 118,-184"/>
|
||||
<text text-anchor="start" x="149.5083" y="-190.8" font-family="Times,serif" font-size="14.00" fill="#000000">key:</text>
|
||||
<text text-anchor="start" x="173.6118" y="-190.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARBINARY</text>
|
||||
<polygon fill="none" stroke="#000000" points="118,-164 118,-184 262,-184 262,-164 118,-164"/>
|
||||
<text text-anchor="start" x="159.394" y="-170.8" font-family="Times,serif" font-size="14.00" fill="#000000">user_id:</text>
|
||||
<text text-anchor="start" x="204.4976" y="-170.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="118,-144 118,-164 262,-164 262,-144 118,-144"/>
|
||||
<text text-anchor="start" x="137.9272" y="-150.8" font-family="Times,serif" font-size="14.00" fill="#000000">comment:</text>
|
||||
<text text-anchor="start" x="193.9136" y="-150.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="118,-124 118,-144 262,-144 262,-124 118,-124"/>
|
||||
<text text-anchor="start" x="120.8271" y="-130.8" font-family="Times,serif" font-size="14.00" fill="#000000">authorized_key:</text>
|
||||
<text text-anchor="start" x="211.0137" y="-130.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
</g>
|
||||
<!-- users -->
|
||||
<g id="node13" class="node">
|
||||
<title>users</title>
|
||||
<polygon fill="#dddddd" stroke="transparent" points="700,-184 700,-204 828,-204 828,-184 700,-184"/>
|
||||
<polygon fill="none" stroke="#000000" points="700,-184 700,-204 828,-204 828,-184 700,-184"/>
|
||||
<text text-anchor="start" x="749.6138" y="-189.8" font-family="Times,serif" font-size="14.00" fill="#000000">users</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-164 700,-184 828,-184 828,-164 700,-164"/>
|
||||
<text text-anchor="start" x="748.5562" y="-170.8" font-family="Times,serif" font-size="14.00" fill="#000000">id:</text>
|
||||
<text text-anchor="start" x="763.3354" y="-170.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-144 700,-164 828,-164 828,-144 700,-144"/>
|
||||
<text text-anchor="start" x="704.501" y="-150.8" font-family="Times,serif" font-size="14.00" fill="#000000">created_at:</text>
|
||||
<text text-anchor="start" x="765.9014" y="-150.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-124 700,-144 828,-144 828,-124 700,-124"/>
|
||||
<text text-anchor="start" x="702.5459" y="-130.8" font-family="Times,serif" font-size="14.00" fill="#000000">updated_at:</text>
|
||||
<text text-anchor="start" x="767.8564" y="-130.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-104 700,-124 828,-124 828,-104 700,-104"/>
|
||||
<text text-anchor="start" x="704.4941" y="-110.8" font-family="Times,serif" font-size="14.00" fill="#000000">deleted_at:</text>
|
||||
<text text-anchor="start" x="765.9082" y="-110.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TIMESTAMP</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-84 700,-104 828,-104 828,-84 700,-84"/>
|
||||
<text text-anchor="start" x="716.9463" y="-90.8" font-family="Times,serif" font-size="14.00" fill="#000000">is_admin:</text>
|
||||
<text text-anchor="start" x="772.167" y="-90.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">TINYINT</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-64 700,-84 828,-84 828,-64 700,-64"/>
|
||||
<text text-anchor="start" x="722.4272" y="-70.8" font-family="Times,serif" font-size="14.00" fill="#000000">email:</text>
|
||||
<text text-anchor="start" x="757.4136" y="-70.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-44 700,-64 828,-64 828,-44 700,-44"/>
|
||||
<text text-anchor="start" x="722.8169" y="-50.8" font-family="Times,serif" font-size="14.00" fill="#000000">name:</text>
|
||||
<text text-anchor="start" x="757.0239" y="-50.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-24 700,-44 828,-44 828,-24 700,-24"/>
|
||||
<text text-anchor="start" x="711.9272" y="-30.8" font-family="Times,serif" font-size="14.00" fill="#000000">comment:</text>
|
||||
<text text-anchor="start" x="767.9136" y="-30.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
<polygon fill="none" stroke="#000000" points="700,-4 700,-24 828,-24 828,-4 700,-4"/>
|
||||
<text text-anchor="start" x="702.9824" y="-10.8" font-family="Times,serif" font-size="14.00" fill="#000000">invite_token:</text>
|
||||
<text text-anchor="start" x="776.8584" y="-10.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">VARCHAR</text>
|
||||
</g>
|
||||
<!-- user_keys->users -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>user_keys:user_id->users:id</title>
|
||||
<path fill="none" stroke="#444444" stroke-dasharray="5,2" d="M262,-174C453.2448,-174 503.5773,-174 689.8681,-174"/>
|
||||
<polygon fill="#444444" stroke="#444444" points="700,-177.5 690,-174.0001 700,-170.5 700,-177.5"/>
|
||||
</g>
|
||||
<!-- user_user_groups -->
|
||||
<g id="node12" class="node">
|
||||
<title>user_user_groups</title>
|
||||
<polygon fill="#dddddd" stroke="transparent" points="137,-370 137,-390 243,-390 243,-370 137,-370"/>
|
||||
<polygon fill="none" stroke="#000000" points="137,-370 137,-390 243,-390 243,-370 137,-370"/>
|
||||
<text text-anchor="start" x="140.6206" y="-375.8" font-family="Times,serif" font-size="14.00" fill="#000000">user_user_groups</text>
|
||||
<polygon fill="none" stroke="#000000" points="137,-350 137,-370 243,-370 243,-350 137,-350"/>
|
||||
<text text-anchor="start" x="159.394" y="-356.8" font-family="Times,serif" font-size="14.00" fill="#000000">user_id:</text>
|
||||
<text text-anchor="start" x="204.4976" y="-356.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
<polygon fill="none" stroke="#000000" points="137,-330 137,-350 243,-350 243,-330 137,-330"/>
|
||||
<text text-anchor="start" x="139.563" y="-336.8" font-family="Times,serif" font-size="14.00" fill="#000000">user_group_id:</text>
|
||||
<text text-anchor="start" x="224.3286" y="-336.8" font-family="Helvetica,sans-Serif" font-style="oblique" font-size="10.00" fill="#000000">INT</text>
|
||||
</g>
|
||||
<!-- user_user_groups->user_groups -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>user_user_groups:user_group_id->user_groups:id</title>
|
||||
<path fill="none" stroke="#444444" stroke-dasharray="5,2" d="M243,-340C442.64,-340 495.1088,-340 689.7185,-340"/>
|
||||
<polygon fill="#444444" stroke="#444444" points="700,-343.5 690,-340.0001 700,-336.5 700,-343.5"/>
|
||||
</g>
|
||||
<!-- user_user_groups->users -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>user_user_groups:user_id->users:id</title>
|
||||
<path fill="none" stroke="#444444" stroke-dasharray="5,2" d="M243,-360C458.8631,-360 480.0858,-179.7671 689.8664,-174.1348"/>
|
||||
<polygon fill="#444444" stroke="#444444" points="700.0466,-177.4997 690.0009,-174.1331 699.9534,-170.5003 700.0466,-177.4997"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 37 KiB |
53
.circleci/config.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
defaults: &defaults
|
||||
working_directory: /go/src/moul.io/sshportal
|
||||
docker:
|
||||
- image: circleci/golang:1.14.0
|
||||
environment:
|
||||
GO111MODULE: "on"
|
||||
|
||||
install_retry: &install_retry
|
||||
run:
|
||||
name: install retry
|
||||
command: |
|
||||
command -v wget &>/dev/null && wget -O /tmp/retry "https://github.com/moul/retry/releases/download/v0.5.0/retry_$(uname -s)_$(uname -m)" || true
|
||||
if [ ! -f /tmp/retry ]; then command -v curl &>/dev/null && curl -L -o /tmp/retry "https://github.com/moul/retry/releases/download/v0.5.0/retry_$(uname -s)_$(uname -m)"; fi
|
||||
chmod +x /tmp/retry
|
||||
/tmp/retry --version
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
go.build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- *install_retry
|
||||
- run: /tmp/retry -m 3 go mod download
|
||||
- run: /tmp/retry -m 3 go mod vendor
|
||||
- run: /tmp/retry -m 3 make install
|
||||
- run: GO111MODULE=off /tmp/retry -m 3 go test -v ./...
|
||||
- run: /tmp/retry -m 3 curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.12.2
|
||||
- run: PATH=$PATH:$(pwd)/bin /tmp/retry -m 3 make lint
|
||||
|
||||
docker.integration:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install Docker Compose
|
||||
command: |
|
||||
umask 022
|
||||
curl -L https://github.com/docker/compose/releases/download/1.11.4/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: true
|
||||
- *install_retry
|
||||
- run: /tmp/retry -m 3 docker build -t moul/sshportal .
|
||||
- run: /tmp/retry -m 3 make integration
|
||||
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_integration:
|
||||
jobs:
|
||||
- go.build
|
||||
- docker.integration
|
||||
# requires: docker.build?
|
||||
@@ -1 +1,6 @@
|
||||
examples
|
||||
# .git/ # should be kept for git-based versionning
|
||||
|
||||
examples/
|
||||
.circleci/
|
||||
.assets/
|
||||
/sshportal
|
||||
|
||||
6
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
github: ["moul"]
|
||||
patreon: moul
|
||||
open_collective: sshportal
|
||||
custom:
|
||||
- "https://www.buymeacoffee.com/moul"
|
||||
- "https://manfred.life/donate"
|
||||
25
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<!-- Thanks for filling an issue!
|
||||
|
||||
If this is a BUG REPORT, please:
|
||||
- Fill in as much of the template below as you can
|
||||
|
||||
If this is a FEATURE REQUEST, please:
|
||||
- Describe *in detail* the feature/behavior/change you would like to see
|
||||
-->
|
||||
|
||||
**What happened**:
|
||||
|
||||
**What you expected to happen**:
|
||||
|
||||
**How to reproduce it (as minimally and precisely as possible)**:
|
||||
|
||||
**Anything else we need to know?**:
|
||||
|
||||
<!--
|
||||
**Environment**:
|
||||
- sshportal --version
|
||||
- ssh sshportal info
|
||||
- OS (e.g. from /etc/os-release):
|
||||
- install method (e.g. go/docker/brew/...):
|
||||
- others:
|
||||
-->
|
||||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<!-- Thanks for sending a pull request! Here are some tips for you -->
|
||||
|
||||
**What this PR does / why we need it**:
|
||||
|
||||
**Which issue this PR fixes**: fixes #xxx, fixes #xxx...
|
||||
|
||||
**Special notes for your reviewer**:
|
||||
13
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Semantic Release
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
semantic-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: codfish/semantic-release-action@v1
|
||||
if: github.ref == 'refs/heads/master'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
9
.gitignore
vendored
@@ -1,2 +1,9 @@
|
||||
dist/
|
||||
*~
|
||||
*#
|
||||
.*#
|
||||
.DS_Store
|
||||
/log/
|
||||
/sshportal
|
||||
*.db
|
||||
*.db
|
||||
/data
|
||||
34
.golangci.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
run:
|
||||
deadline: 1m
|
||||
tests: false
|
||||
#skip-files:
|
||||
# - ".*\\.gen\\.go"
|
||||
|
||||
linters-settings:
|
||||
golint:
|
||||
min-confidence: 0
|
||||
maligned:
|
||||
suggest-new: true
|
||||
goconst:
|
||||
min-len: 5
|
||||
min-occurrences: 4
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- goconst
|
||||
- misspell
|
||||
- deadcode
|
||||
- misspell
|
||||
- structcheck
|
||||
- errcheck
|
||||
- unused
|
||||
- varcheck
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- gofmt
|
||||
- goimports
|
||||
- golint
|
||||
- ineffassign
|
||||
29
.goreleaser.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
builds:
|
||||
-
|
||||
goos: [linux, darwin]
|
||||
goarch: [386, amd64, arm, arm64]
|
||||
ldflags:
|
||||
- -s -w -X main.GitSha={{.ShortCommit}} -X main.GitBranch=master -X main.GitTag={{.Version}}
|
||||
archives:
|
||||
- wrap_in_directory: true
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
brews:
|
||||
-
|
||||
name: sshportal
|
||||
github:
|
||||
owner: moul
|
||||
name: homebrew-moul
|
||||
commit_author:
|
||||
name: moul-bot
|
||||
email: "m+bot@42.am"
|
||||
homepage: https://manfred.life/sshportal
|
||||
description: "Simple, fun and transparent SSH (and telnet) bastion"
|
||||
8
.releaserc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
branch: 'master',
|
||||
plugins: [
|
||||
'@semantic-release/commit-analyzer',
|
||||
'@semantic-release/release-notes-generator',
|
||||
'@semantic-release/github',
|
||||
],
|
||||
};
|
||||
102
CHANGELOG.md
@@ -1,6 +1,104 @@
|
||||
# Changelog
|
||||
|
||||
## 1.1.0 (2017-11-15)
|
||||
## master (unreleased)
|
||||
|
||||
* No entry
|
||||
|
||||
## v1.10.0 (2019-06-24)
|
||||
|
||||
* Bump deps, now using github.com/gliderlabs/ssh upstream
|
||||
* Fix Windows build ([#101](https://github.com/moul/sshportal/pull/101)) by [@Raerten](https://github.com/Raerten)
|
||||
* Use environment variables for settings ([#98](https://github.com/moul/sshportal/pull/98)) by [@Raerten](https://github.com/Raerten)
|
||||
* Fix 'userkey create' ([#111](https://github.com/moul/sshportal/pull/111)) by [@shawn111](https://github.com/shawn111)
|
||||
* Set log files mode to 440 instead of 640 ([#134](https://github.com/moul/sshportal/pull/134)) by [@jle64](https://github.com/jle64)
|
||||
* Allow to create a host using an IP as name ([#135](https://github.com/moul/sshportal/pull/135)) by [@jle64](https://github.com/jle64)
|
||||
* Add username and session ID to session log filename ([#133](https://github.com/moul/sshportal/pull/133)) by [@jle64](https://github.com/jle64)
|
||||
* Unable to use encrypted SSH private keys ([#124](https://github.com/moul/sshportal/pull/124)) by [@welderpb](https://github.com/welderpb)
|
||||
* Fix format of ID in new session + closing channel if host is unreachable ([#123](https://github.com/moul/sshportal/pull/123)) by [@vdaviot](https://github.com/vdaviot)
|
||||
* Refactor the main package with a focus on splitting up into packages ([#113](https://github.com/moul/sshportal/pull/113)) by [@ahamidullah](https://github.com/ahamidullah)
|
||||
|
||||
|
||||
## v1.9.0 (2018-11-18)
|
||||
|
||||
* Add `hostgroup update` and `usergroup update` commands ([#58](https://github.com/moul/sshportal/pull/58)) by [@adyxax](https://github.com/adyxax)
|
||||
* Add socket timeout ([#80](https://github.com/moul/sshportal/pull/80)) by [@ahhx](https://github.com/ahhx)
|
||||
* Add a flag to list only active sessions ([#76](https://github.com/moul/sshportal/pull/76)) by [@vdaviot](https://github.com/vdaviot)
|
||||
* Unset hop on host ([#74](https://github.com/moul/sshportal/pull/74)) by [@vdaviot](https://github.com/vdaviot)
|
||||
* Fix session status and duration display ([#75](https://github.com/moul/sshportal/pull/75)) by [@vdaviot](https://github.com/vdaviot)
|
||||
* Fix log path and filename on Windows ([#78](https://github.com/moul/sshportal/pull/78)) by [@Raerten](https://github.com/Raerten)
|
||||
* Admin user is not editable ([#69](https://github.com/moul/sshportal/pull/69)) by [@alenn-m](https://github.com/alenn-m)
|
||||
* Switch to go modules (go1.11) ([#83](https://github.com/moul/sshportal/pull/83))
|
||||
* Switch to moul.io/sshportal canonical URL ([#86](https://github.com/moul/sshportal/pull/86))
|
||||
* Switch to golangci-lint ([#87](https://github.com/moul/sshportal/pull/87))
|
||||
|
||||
## v1.8.0 (2018-04-02)
|
||||
|
||||
* The default created user now has the same username as the user starting sshportal (was hardcoded "admin")
|
||||
* Add Telnet support
|
||||
* Add TTY audit feature ([#23](https://github.com/moul/sshportal/issues/23)) by [@sabban](https://github.com/sabban)
|
||||
* Fix `--assign-*` commands when using MySQL driver ([#45](https://github.com/moul/sshportal/issues/45))
|
||||
* Add *HOP* support, an efficient and integrated way of using a jump host transparently ([#47](https://github.com/moul/sshportal/issues/47)) by [@mathieui](https://github.com/mathieui)
|
||||
* Fix panic on some `ls` commands ([#54](https://github.com/moul/sshportal/pull/54)) by [@jle64](https://github.com/jle64)
|
||||
* Add tunnels (`direct-tcp`) support with logging ([#44](https://github.com/moul/sshportal/issues/44)) by [@sabban](https://github.com/sabban)
|
||||
* Add `key import` command ([#52](https://github.com/moul/sshportal/issues/52)) by [@adyxax](https://github.com/adyxax)
|
||||
* Add 'exec' logging ([#40](https://github.com/moul/sshportal/issues/40)) by [@sabban](https://github.com/sabban)
|
||||
|
||||
## v1.7.1 (2018-01-03)
|
||||
|
||||
* Return non-null exit-code on authentication error
|
||||
* **hotfix**: repair invite system (broken in v1.7.0)
|
||||
|
||||
## v1.7.0 (2018-01-02)
|
||||
|
||||
Breaking changes:
|
||||
* Use `sshportal server` instead of `sshportal` to start a new server (nothing to change if using the docker image)
|
||||
* Remove `--config-user` and `--healthcheck-user` global options
|
||||
|
||||
Changes:
|
||||
* Fix connection failure when sending too many environment variables (fix [#22](https://github.com/moul/sshportal/issues/22))
|
||||
* Fix panic when entering empty command (fix [#13](https://github.com/moul/sshportal/issues/13))
|
||||
* Add `config backup --ignore-events` option
|
||||
* Add `sshportal healthcheck [--addr=] [--wait] [--quiet]` cli command
|
||||
* Add [Docker Healthcheck](https://docs.docker.com/engine/reference/builder/#healthcheck) helper
|
||||
* Support Putty (fix [#24](https://github.com/moul/sshportal/issues/24))
|
||||
|
||||
## v1.6.0 (2017-12-12)
|
||||
|
||||
* Add `--latest` and `--quiet` options to `ls` commands
|
||||
* Add `healthcheck` user
|
||||
* Add `key show KEY` command
|
||||
|
||||
## v1.5.0 (2017-12-02)
|
||||
|
||||
* Create Session objects on each connections (history)
|
||||
* Connection history
|
||||
* Audit log
|
||||
* Add dynamic strict host key checking (learning on the first time, strict on the next ones)
|
||||
* Add-back MySQL support (experimental)
|
||||
* Fix some backup/restore bugs
|
||||
|
||||
## v1.4.0 (2017-11-24)
|
||||
|
||||
* Add 'key setup' command (easy SSH key installation)
|
||||
* Add Updated and Created fields in 'ls' commands
|
||||
* Add `--aes-key` option to encrypt sensitive data
|
||||
|
||||
## v1.3.0 (2017-11-23)
|
||||
|
||||
* More details in 'ls' commands
|
||||
* Add 'host update' command (fix [#2](https://github.com/moul/sshportal/issues/2))
|
||||
* Add 'user update' command (fix [#3](https://github.com/moul/sshportal/issues/3))
|
||||
* Add 'acl update' command (fix [#4](https://github.com/moul/sshportal/issues/4))
|
||||
* Allow connecting to the shell mode with the registered username or email (fix [#5](https://github.com/moul/sshportal/issues/5))
|
||||
* Add 'listhosts' role (fix [#5](https://github.com/moul/sshportal/issues/5))
|
||||
|
||||
## v1.2.0 (2017-11-22)
|
||||
|
||||
* Support adding multiple `--group` links on `host create` and `user create`
|
||||
* Use govalidator to perform more consistent input validation
|
||||
* Use a database migration system
|
||||
|
||||
## v1.1.0 (2017-11-15)
|
||||
|
||||
* Improve versionning (static VERSION + dynamic GIT_* info)
|
||||
* Configuration management (backup + restore)
|
||||
@@ -8,7 +106,7 @@
|
||||
* Disable mysql support (not fully working right now)
|
||||
* Set random seed properly
|
||||
|
||||
## 1.0.0 (2017-11-14)
|
||||
## v1.0.0 (2017-11-14)
|
||||
|
||||
Initial version
|
||||
|
||||
|
||||
14
Dockerfile
@@ -1,10 +1,16 @@
|
||||
# build
|
||||
FROM golang:1.9 as builder
|
||||
COPY . /go/src/github.com/moul/sshportal
|
||||
WORKDIR /go/src/github.com/moul/sshportal
|
||||
FROM golang:1.14.0 as builder
|
||||
ENV GO111MODULE=on
|
||||
WORKDIR /go/src/moul.io/sshportal
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . ./
|
||||
RUN make _docker_install
|
||||
|
||||
# minimal runtime
|
||||
FROM scratch
|
||||
FROM alpine
|
||||
COPY --from=builder /go/bin/sshportal /bin/sshportal
|
||||
ENTRYPOINT ["/bin/sshportal"]
|
||||
CMD ["server"]
|
||||
EXPOSE 2222
|
||||
HEALTHCHECK CMD /bin/sshportal healthcheck --wait
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
@@ -176,13 +175,24 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2013-2017 Docker, Inc.
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017 Manfred Touron <m@42.am>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
50
Makefile
@@ -1,22 +1,58 @@
|
||||
GIT_SHA ?= $(shell git rev-parse HEAD)
|
||||
GIT_TAG ?= $(shell git describe --tags --always)
|
||||
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
LDFLAGS ?= -X main.GIT_SHA=$(GIT_SHA) -X main.GIT_TAG=$(GIT_TAG) -X main.GIT_BRANCH=$(GIT_BRANCH)
|
||||
LDFLAGS ?= -X main.GitSha=$(GIT_SHA) -X main.GitTag=$(GIT_TAG) -X main.GitBranch=$(GIT_BRANCH)
|
||||
VERSION ?= $(shell grep 'VERSION =' main.go | cut -d'"' -f2)
|
||||
AES_KEY ?= my-dummy-aes-key
|
||||
GO ?= GO111MODULE=on go
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
go install -ldflags '$(LDFLAGS)' .
|
||||
$(GO) install -v -ldflags '$(LDFLAGS)' .
|
||||
|
||||
.PHONY: docker.build
|
||||
docker.build:
|
||||
docker build -t moul/sshportal .
|
||||
|
||||
.PHONY: integration
|
||||
integration:
|
||||
cd ./examples/integration && make
|
||||
|
||||
.PHONY: _docker_install
|
||||
_docker_install:
|
||||
CGO_ENABLED=1 go build -ldflags '-extldflags "-static" $(LDFLAGS)' -tags netgo -v -o /go/bin/sshportal
|
||||
CGO_ENABLED=1 $(GO) build -ldflags '-extldflags "-static" $(LDFLAGS)' -tags netgo -v -o /go/bin/sshportal
|
||||
|
||||
.PHONY: dev
|
||||
dev:
|
||||
-go get github.com/githubnemo/CompileDaemon
|
||||
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal --demo --debug" .
|
||||
-$(GO) get github.com/githubnemo/CompileDaemon
|
||||
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal server --debug --bind-address=:$(PORT) --aes-key=$(AES_KEY) $(EXTRA_RUN_OPTS)" .
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -i .
|
||||
go test -v .
|
||||
$(GO) test -i ./...
|
||||
$(GO) test -v ./...
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
golangci-lint run --verbose ./...
|
||||
|
||||
.PHONY: backup
|
||||
backup:
|
||||
mkdir -p data/backups
|
||||
cp sshportal.db data/backups/$(shell date +%s)-$(VERSION)-sshportal.sqlite
|
||||
|
||||
doc:
|
||||
dot -Tsvg ./.assets/overview.dot > ./.assets/overview.svg
|
||||
dot -Tsvg ./.assets/cluster-mysql.dot > ./.assets/cluster-mysql.svg
|
||||
dot -Tsvg ./.assets/flow-diagram.dot > ./.assets/flow-diagram.svg
|
||||
dot -Tpng ./.assets/overview.dot > ./.assets/overview.png
|
||||
dot -Tpng ./.assets/cluster-mysql.dot > ./.assets/cluster-mysql.png
|
||||
dot -Tpng ./.assets/flow-diagram.dot > ./.assets/flow-diagram.png
|
||||
|
||||
.PHONY: goreleaser
|
||||
goreleaser:
|
||||
GORELEASER_GITHUB_TOKEN=$(GORELEASER_GITHUB_TOKEN) GITHUB_TOKEN=$(GITHUB_TOKEN) goreleaser --rm-dist
|
||||
|
||||
.PHONY: goreleaser-dry-run
|
||||
goreleaser-dry-run:
|
||||
goreleaser --snapshot --skip-publish --rm-dist
|
||||
|
||||
477
README.md
@@ -1,43 +1,48 @@
|
||||
# sshportal
|
||||
|
||||
[](https://circleci.com/gh/moul/sshportal)
|
||||
[](https://goreportcard.com/report/moul.io/sshportal)
|
||||
[](https://godoc.org/moul.io/sshportal)
|
||||
[](https://opencollective.com/sshportal) [](https://github.com/moul/sshportal/blob/master/LICENSE)
|
||||
[](https://github.com/moul/sshportal/releases)
|
||||
<!-- temporarily broken? [](https://hub.docker.com/r/moul/sshportal/) -->
|
||||
|
||||
Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
|
||||
|
||||
```
|
||||
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
DMZ │
|
||||
┌────────┐ │ ┌────────┐
|
||||
│ homer │───▶╔═════════════════╗───▶│ host1 │ │
|
||||
└────────┘ ║ ║ └────────┘
|
||||
┌────────┐ ║ ║ ┌────────┐ │
|
||||
│ bart │───▶║ sshportal ║───▶│ host2 │
|
||||
└────────┘ ║ ║ └────────┘ │
|
||||
┌────────┐ ║ ║ ┌────────┐
|
||||
│ lisa │───▶╚═════════════════╝───▶│ host3 │ │
|
||||
└────────┘ │ └────────┘
|
||||
┌────────┐ ┌────────┐ │
|
||||
│ ... │ │ │ ... │
|
||||
└────────┘ └────────┘ │
|
||||
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
```
|
||||
Features include: independence of users and hosts, convenient user invite system, connecting to servers that don't support SSH keys, various levels of access, and many more. Easy to install, run and configure.
|
||||
|
||||
## Features
|
||||

|
||||
|
||||
* Host management
|
||||
* User management
|
||||
* User Group management
|
||||
* Host Group management
|
||||
* Host Key management
|
||||
* User Key management
|
||||
* ACL management
|
||||
* Connect to host using key or password
|
||||
* Admin commands can be run directly or in an interactive shell
|
||||
---
|
||||
|
||||
## Usage
|
||||
## Contents
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
- [Installation and usage](#installation-and-usage)
|
||||
- [Use cases](#use-cases)
|
||||
- [Features and limitations](#features-and-limitations)
|
||||
- [Docker](#docker)
|
||||
- [Manual Install](#manual-install)
|
||||
- [Backup / Restore](#backup--restore)
|
||||
- [built-in shell](#built-in-shell)
|
||||
- [Demo data](#demo-data)
|
||||
- [Shell commands](#shell-commands)
|
||||
- [Healthcheck](#healthcheck)
|
||||
- [portal alias (.ssh/config)](#portal-alias-sshconfig)
|
||||
- [Scaling](#scaling)
|
||||
- [Under the hood](#under-the-hood)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
---
|
||||
|
||||
## Installation and usage
|
||||
|
||||
Start the server
|
||||
|
||||
```console
|
||||
$ sshportal
|
||||
$ sshportal server
|
||||
2017/11/13 10:58:35 Admin user created, use the user 'invite:BpLnfgDsc2WD8F2q' to associate a public key with this account
|
||||
2017/11/13 10:58:35 SSH Server accepting connections on :2222
|
||||
```
|
||||
@@ -46,7 +51,7 @@ Link your SSH key with the admin account
|
||||
|
||||
```console
|
||||
$ ssh localhost -p 2222 -l invite:BpLnfgDsc2WD8F2q
|
||||
Welcome Administrator!
|
||||
Welcome admin!
|
||||
|
||||
Your key is now associated with the user "admin@sshportal".
|
||||
Shared connection to localhost closed.
|
||||
@@ -80,28 +85,18 @@ List hosts
|
||||
|
||||
```console
|
||||
config> host ls
|
||||
ID | NAME | URL | KEY | PASS | GROUPS | COMMENT
|
||||
+----+------+-------------------------+---------+------+--------+---------+
|
||||
1 | foo | bart@foo.example.org:22 | default | | 1 |
|
||||
ID | NAME | URL | KEY | PASS | GROUPS | COMMENT
|
||||
+----+------+-------------------------+---------+------+---------+---------+
|
||||
1 | foo | bart@foo.example.org:22 | default | | default |
|
||||
Total: 1 hosts.
|
||||
config>
|
||||
```
|
||||
|
||||
Get the default key in authorized_keys format
|
||||
Add the key to the server
|
||||
|
||||
```console
|
||||
config> key inspect default
|
||||
[...]
|
||||
"PubKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvUP/8FedyIe+a+RWU4KvJ1+iZwtWmY9czJubLwN4RcjKHQMzLqWC7pKZHAABCZjLJjVD/3Zb53jZwbh7mysAkocundMpvUL5+Yb4a8lDiflXkdXT9fZCx+ibJBk4jRnKLGIneSzVtFEerEwQKKnKQoCgPkZwCDaL/jHhDlOmAvxqAJrjiy42HXwppX2UuF8zujs6OKHRYJ/Q1vo0caa6/o1eoyXE9OrOwIk+IcAN3YIQi/B1BOlZOQBzHIZz83AFlD2TcPhyYcbxPyKGih84Zr3rQaaP1WiaiPqxzp3s5OhTLthc5XtCSLzmRSLvgC2eFdNhBDB5KLtO2khBkz5ID",
|
||||
[...]
|
||||
config>
|
||||
```
|
||||
|
||||
Add this key to the server
|
||||
|
||||
```console
|
||||
$ ssh bart@foo.example.org
|
||||
> umask 077; mkdir -p .ssh; echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvUP/8FedyIe+a+RWU4KvJ1+iZwtWmY9czJubLwN4RcjKHQMzLqWC7pKZHAABCZjLJjVD/3Zb53jZwbh7mysAkocundMpvUL5+Yb4a8lDiflXkdXT9fZCx+ibJBk4jRnKLGIneSzVtFEerEwQKKnKQoCgPkZwCDaL/jHhDlOmAvxqAJrjiy42HXwppX2UuF8zujs6OKHRYJ/Q1vo0caa6/o1eoyXE9OrOwIk+IcAN3YIQi/B1BOlZOQBzHIZz83AFlD2TcPhyYcbxPyKGih84Zr3rQaaP1WiaiPqxzp3s5OhTLthc5XtCSLzmRSLvgC2eFdNhBDB5KLtO2khBkz5ID >> .ssh/authorized_keys
|
||||
$ ssh bart@foo.example.org "$(ssh localhost -p 2222 -l admin key setup default)"
|
||||
$
|
||||
```
|
||||
|
||||
Profit
|
||||
@@ -113,112 +108,145 @@ bart@foo>
|
||||
|
||||
Invite friends
|
||||
|
||||
*This command doesn't create a user on the remote server, it only creates an account in the sshportal database.*
|
||||
|
||||
```console
|
||||
config> user invite bob@example.com
|
||||
User 2 created.
|
||||
To associate this account with a key, use the following SSH user: 'invite-NfHK5a84jjJkwzDk'.
|
||||
To associate this account with a key, use the following SSH user: 'invite:NfHK5a84jjJkwzDk'.
|
||||
config>
|
||||
```
|
||||
|
||||
## CLI
|
||||
Demo gif:
|
||||

|
||||
|
||||
sshportal embeds a configuration CLI.
|
||||
---
|
||||
|
||||
By default, the configuration user is `admin`, (can be changed using `--config-user=<value>` when starting the server.
|
||||
## Use cases
|
||||
|
||||
Each commands can be run directly by using this syntax: `ssh admin@portal.example.org <command> [args]`:
|
||||
Used by educators to provide temporary access to students. [Feedback from a teacher](https://github.com/moul/sshportal/issues/64). The author is using it in one of his projects, *pathwar*, to dynamically configure hosts and users, so that he can give temporary accesses for educational purposes.
|
||||
|
||||
```
|
||||
ssh admin@portal.example.org host inspect toto
|
||||
```
|
||||
*vptech*, the vente-privee.com technical team (a group of over 6000 people) is using it internally to manage access to servers/routers, saving hours on configuration management and not having to share the configuration information.
|
||||
|
||||
You can enter in interactive mode using this syntax: `ssh admin@portal.example.org`
|
||||
There are companies who use a jump host to monitor connections at a single point.
|
||||
|
||||
### Synopsis
|
||||
A hosting company is using SSHportal for its “logging” feature, among the others. As every session is logged and introspectable, they have a detailed history of who performed which action. This company made its own contribution on the project, allowing the support of [more than 65.000 sessions in the database](https://github.com/moul/sshportal/pull/76).
|
||||
|
||||
```sh
|
||||
# acl management
|
||||
acl help
|
||||
acl create [-h] [--hostgroup=<value>...] [--usergroup=<value>...] [--pattern=<value>] [--comment=<value>] [--action=<value>] [--weight=value]
|
||||
acl inspect [-h] <id> [<id> [<id>...]]
|
||||
acl ls [-h]
|
||||
acl rm [-h] <id> [<id> [<id>...]]
|
||||
The project has also received [multiple contributions from a security researcher](https://github.com/moul/sshportal/pulls?q=is%3Apr+author%3Asabban+sort%3Aupdated-desc) that made a thesis on quantum cryptography. This person uses SSHportal in their security-hardened hosting company.
|
||||
|
||||
# config management
|
||||
config help
|
||||
config backup [-h] [--indent]
|
||||
config restore [-h] [--confirm]
|
||||
If you need to invite multiple people to an event (hackathon, course, etc), the day before the event you can create multiple accounts at once, print the invite, and distribute the paper.
|
||||
|
||||
# host management
|
||||
host help
|
||||
host create [-h] [--name=<value>] [--password=<value>] [--fingerprint=<value>] [--comment=<value>] [--key=<value>] [--group=<value>] <user>[:<password>]@<host>[:<port>]
|
||||
host inspect [-h] <id or name> [<id or name> [<id or name>...]]
|
||||
host ls [-h]
|
||||
host rm [-h] <id or name> [<id or name> [<id or name>...]]
|
||||
---
|
||||
|
||||
# hostgroup management
|
||||
hostgroup help
|
||||
hostgroup create [-h] [--name=<value>] [--comment=<value>]
|
||||
hostgroup inspect [-h] <id or name> [<id or name> [<id or name>...]]
|
||||
hostgroup ls [-h]
|
||||
hostgroup rm [-h] <id or name> [<id or name> [<id or name>...]]
|
||||
## Features and limitations
|
||||
|
||||
# key management
|
||||
key help
|
||||
key create [-h] [--name=<value>] [--type=<value>] [--length=<value>] [--comment=<value>]
|
||||
key inspect [-h] <id or name> [<id or name> [<id or name>...]]
|
||||
key ls [-h]
|
||||
key rm [-h] <id or name> [<id or name> [<id or name>...]]
|
||||
* Single autonomous binary (~10-20Mb) with no runtime dependencies (embeds ssh server and client)
|
||||
* Portable / Cross-platform (regularly tested on linux and OSX/darwin)
|
||||
* Store data in [Sqlite3](https://www.sqlite.org/) or [MySQL](https://www.mysql.com) (probably easy to add postgres, mssql thanks to gorm)
|
||||
* Stateless -> horizontally scalable when using [MySQL](https://www.mysql.com) as the backend
|
||||
* Connect to remote host using key or password
|
||||
* Admin commands can be run directly or in an interactive shell
|
||||
* Host management
|
||||
* User management (invite, group, stats)
|
||||
* Host Key management (create, remove, update, import)
|
||||
* Automatic remote host key learning
|
||||
* User Key management (multile keys per user)
|
||||
* ACL management (acl+user-groups+host-groups)
|
||||
* User roles (admin, trusted, standard, ...)
|
||||
* User invitations (no more "give me your public ssh key please")
|
||||
* Easy server installation (generate shell command to setup `authorized_keys`)
|
||||
* Sensitive data encryption
|
||||
* Session management (see active connections, history, stats, stop)
|
||||
* Audit log (logging every user action)
|
||||
* Record TTY Session
|
||||
* Tunnels logging
|
||||
* Host Keys verifications shared across users
|
||||
* Healthcheck user (replying OK to any user)
|
||||
* SSH compatibility
|
||||
* ipv4 and ipv6 support
|
||||
* [`scp`](https://linux.die.net/man/1/scp) support
|
||||
* [`rsync`](https://linux.die.net/man/1/rsync) support
|
||||
* [tunneling](https://www.ssh.com/ssh/tunneling/example) (local forward, remote forward, dynamic forward) support
|
||||
* [`sftp`](https://www.ssh.com/ssh/sftp/) support
|
||||
* [`ssh-agent`](https://www.ssh.com/ssh/agent) support
|
||||
* [`X11 forwarding`](http://en.tldp.org/HOWTO/XDMCP-HOWTO/ssh.html) support
|
||||
* Git support (can be used to easily use multiple user keys on GitHub, or access your own firewalled gitlab server)
|
||||
* Do not require any SSH client modification or custom `.ssh/config`, works with every tested SSH programming libraries and every tested SSH clients
|
||||
* SSH to non-SSH proxy
|
||||
* [Telnet](https://www.ssh.com/ssh/telnet) support
|
||||
|
||||
# user management
|
||||
user help
|
||||
user invite [-h] [--name=<value>] [--comment=<value>] [--group=<value>] <email>
|
||||
user inspect [-h] <id or email> [<id or email> [<id or email>...]]
|
||||
user ls [-h]
|
||||
user rm [-h] <id or email> [<id or email> [<id or email>...]]
|
||||
**(Known) limitations**
|
||||
|
||||
# usergroup management
|
||||
usergroup help
|
||||
hostgroup create [-h] [--name=<value>] [--comment=<value>]
|
||||
usergroup inspect [-h] <id or name> [<id or name> [<id or name>...]]
|
||||
usergroup ls [-h]
|
||||
usergroup rm [-h] <id or name> [<id or name> [<id or name>...]]
|
||||
* Does not work (yet?) with [`mosh`](https://mosh.org/)
|
||||
|
||||
# other
|
||||
exit [-h]
|
||||
help, h
|
||||
info [-h]
|
||||
version [-h]
|
||||
```
|
||||
---
|
||||
|
||||
## Docker
|
||||
|
||||
Docker is the recommended way to run sshportal.
|
||||
|
||||
An [automated build is setup on the Docker Hub](https://hub.docker.com/r/moul/sshportal/tags/).
|
||||
|
||||
```console
|
||||
# Start a server in background
|
||||
# mount `pwd` to persist the sqlite database file
|
||||
docker run -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.1.0
|
||||
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.10.0
|
||||
|
||||
# check logs (mandatory on first run to get the administrator invite token)
|
||||
docker logs -f sshportal
|
||||
```
|
||||
|
||||
The easier way to upgrade sshportal is to do the following:
|
||||
|
||||
```sh
|
||||
# we consider you were using an old version and you want to use the new version v1.10.0
|
||||
|
||||
# stop and rename the last working container + backup the database
|
||||
docker stop sshportal
|
||||
docker rename sshportal sshportal_old
|
||||
cp sshportal.db sshportal.db.bkp
|
||||
|
||||
# run the new version
|
||||
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.10.0
|
||||
# check the logs for migration or cross-version incompabitility errors
|
||||
docker logs -f sshportal
|
||||
```
|
||||
|
||||
Now you can test ssh-ing to sshportal to check if everything looks OK.
|
||||
|
||||
In case of problem, you can rollback to the latest working version with the latest working backup, using:
|
||||
|
||||
```sh
|
||||
docker stop sshportal
|
||||
docker rm sshportal
|
||||
cp sshportal.db.bkp sshportal.db
|
||||
docker rename sshportal_old sshportal
|
||||
docker start sshportal
|
||||
docker logs -f sshportal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manual Install
|
||||
|
||||
Get the latest version using GO.
|
||||
|
||||
```sh
|
||||
go get -u github.com/moul/sshportal
|
||||
go get -u moul.io/sshportal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backup / Restore
|
||||
|
||||
sshportal embeds built-in backup/restore methods which basically import/export JSON objects:
|
||||
|
||||
```sh
|
||||
# Backup
|
||||
ssh admin@sshportal config backup > sshportal.bkp
|
||||
ssh portal config backup > sshportal.bkp
|
||||
|
||||
# Restore
|
||||
ssh admin@sshportal config restore < sshporta.bkp
|
||||
ssh portal config restore < sshportal.bkp
|
||||
```
|
||||
|
||||
This method is particularly useful as it should be resistant against future DB schema changes (expected during development phase).
|
||||
@@ -232,3 +260,238 @@ sqlite3 sshportal.db .dump > sshportal.sql.bkp
|
||||
# or just the immortal cp
|
||||
cp sshportal.db sshportal.db.bkp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## built-in shell
|
||||
|
||||
`sshportal` embeds a configuration CLI.
|
||||
|
||||
By default, the configuration user is `admin`, (can be changed using `--config-user=<value>` when starting the server.
|
||||
|
||||
Each commands can be run directly by using this syntax: `ssh admin@portal.example.org <command> [args]`:
|
||||
|
||||
```
|
||||
ssh admin@portal.example.org host inspect toto
|
||||
```
|
||||
|
||||
You can enter in interactive mode using this syntax: `ssh admin@portal.example.org`
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Demo data
|
||||
|
||||
The following servers are freely available, without external registration,
|
||||
it makes it easier to quickly test `sshportal` without configuring your own servers to accept sshportal connections.
|
||||
|
||||
```
|
||||
ssh portal host create new@sdf.org
|
||||
ssh sdf@portal
|
||||
|
||||
ssh portal host create test@whoami.filippo.io
|
||||
ssh whoami@portal
|
||||
|
||||
ssh portal host create test@chat.shazow.net
|
||||
ssh chat@portal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shell commands
|
||||
|
||||
```sh
|
||||
# acl management
|
||||
acl help
|
||||
acl create [-h] [--hostgroup=HOSTGROUP...] [--usergroup=USERGROUP...] [--pattern=<value>] [--comment=<value>] [--action=<value>] [--weight=value]
|
||||
acl inspect [-h] ACL...
|
||||
acl ls [-h] [--latest] [--quiet]
|
||||
acl rm [-h] ACL...
|
||||
acl update [-h] [--comment=<value>] [--action=<value>] [--weight=<value>] [--assign-hostgroup=HOSTGROUP...] [--unassign-hostgroup=HOSTGROUP...] [--assign-usergroup=USERGROUP...] [--unassign-usergroup=USERGROUP...] ACL...
|
||||
|
||||
# config management
|
||||
config help
|
||||
config backup [-h] [--indent] [--decrypt]
|
||||
config restore [-h] [--confirm] [--decrypt]
|
||||
|
||||
# event management
|
||||
event help
|
||||
event ls [-h] [--latest] [--quiet]
|
||||
event inspect [-h] EVENT...
|
||||
|
||||
# host management
|
||||
host help
|
||||
host create [-h] [--name=<value>] [--password=<value>] [--comment=<value>] [--key=KEY] [--group=HOSTGROUP...] [--hop=HOST] <username>[:<password>]@<host>[:<port>]
|
||||
host inspect [-h] [--decrypt] HOST...
|
||||
host ls [-h] [--latest] [--quiet]
|
||||
host rm [-h] HOST...
|
||||
host update [-h] [--name=<value>] [--comment=<value>] [--key=KEY] [--assign-group=HOSTGROUP...] [--unassign-group=HOSTGROUP...] [--set-hop=HOST] [--unset-hop] HOST...
|
||||
|
||||
# hostgroup management
|
||||
hostgroup help
|
||||
hostgroup create [-h] [--name=<value>] [--comment=<value>]
|
||||
hostgroup inspect [-h] HOSTGROUP...
|
||||
hostgroup ls [-h] [--latest] [--quiet]
|
||||
hostgroup rm [-h] HOSTGROUP...
|
||||
|
||||
# key management
|
||||
key help
|
||||
key create [-h] [--name=<value>] [--type=<value>] [--length=<value>] [--comment=<value>]
|
||||
key import [-h] [--name=<value>] [--comment=<value>]
|
||||
key inspect [-h] [--decrypt] KEY...
|
||||
key ls [-h] [--latest] [--quiet]
|
||||
key rm [-h] KEY...
|
||||
key setup [-h] KEY
|
||||
key show [-h] KEY
|
||||
|
||||
# session management
|
||||
session help
|
||||
session ls [-h] [--latest] [--quiet]
|
||||
session inspect [-h] SESSION...
|
||||
|
||||
# user management
|
||||
user help
|
||||
user invite [-h] [--name=<value>] [--comment=<value>] [--group=USERGROUP...] <email>
|
||||
user inspect [-h] USER...
|
||||
user ls [-h] [--latest] [--quiet]
|
||||
user rm [-h] USER...
|
||||
user update [-h] [--name=<value>] [--email=<value>] [--set-admin] [--unset-admin] [--assign-group=USERGROUP...] [--unassign-group=USERGROUP...] USER...
|
||||
|
||||
# usergroup management
|
||||
usergroup help
|
||||
hostgroup create [-h] [--name=<value>] [--comment=<value>]
|
||||
usergroup inspect [-h] USERGROUP...
|
||||
usergroup ls [-h] [--latest] [--quiet]
|
||||
usergroup rm [-h] USERGROUP...
|
||||
|
||||
# other
|
||||
exit [-h]
|
||||
help, h
|
||||
info [-h]
|
||||
version [-h]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Healthcheck
|
||||
|
||||
By default, `sshportal` will return `OK` to anyone sshing using the `healthcheck` user without checking for authentication.
|
||||
|
||||
```console
|
||||
$ ssh healthcheck@sshportal
|
||||
OK
|
||||
$
|
||||
```
|
||||
|
||||
the `healtcheck` user can be changed using the `healthcheck-user` option.
|
||||
|
||||
---
|
||||
|
||||
Alternatively, you can run the built-in healthcheck helper (requiring no ssh client nor ssh key):
|
||||
|
||||
Usage: `sshportal healthcheck [--addr=host:port] [--wait] [--quiet]
|
||||
|
||||
```console
|
||||
$ sshportal healthcheck --addr=localhost:2222; echo $?
|
||||
$ 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Wait for sshportal to be healthy, then connect
|
||||
|
||||
```console
|
||||
$ sshportal healthcheck --wait && ssh sshportal -l admin
|
||||
config>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## portal alias (.ssh/config)
|
||||
|
||||
Edit your `~/.ssh/config` file (create it first if needed)
|
||||
|
||||
```ini
|
||||
Host portal
|
||||
User admin
|
||||
Port 2222 # portal port
|
||||
HostName 127.0.0.1 # portal hostname
|
||||
```
|
||||
|
||||
```bash
|
||||
# you can now run a shell using this:
|
||||
ssh portal
|
||||
# instead of this:
|
||||
ssh localhost -p 2222 -l admin
|
||||
|
||||
# or connect to hosts using this:
|
||||
ssh hostname@portal
|
||||
# instead of this:
|
||||
ssh localhost -p 2222 -l hostname
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scaling
|
||||
|
||||
`sshportal` is stateless but relies on a database to store configuration and logs.
|
||||
|
||||
By default, `sshportal` uses a local [sqlite](https://www.sqlite.org/) database which isn't scalable by design.
|
||||
|
||||
You can run multiple instances of `sshportal` sharing a same [MySQL](https://www.mysql.com) database, using `sshportal --db-conn=user:pass@host/dbname?parseTime=true --db-driver=mysql`.
|
||||
|
||||

|
||||
|
||||
See [examples/mysql](http://github.com/moul/sshportal/tree/master/examples/mysql).
|
||||
|
||||
---
|
||||
|
||||
## Under the hood
|
||||
|
||||
* Docker first (used in dev, tests, by the CI and in production)
|
||||
* Backed by (see [dep graph](https://godoc.org/github.com/moul/sshportal?import-graph&hide=2)):
|
||||
* SSH
|
||||
* https://github.com/gliderlabs/ssh: SSH server made easy (well-designed golang library to build SSH servers)
|
||||
* https://godoc.org/golang.org/x/crypto/ssh: both client and server SSH protocol and helpers
|
||||
* Database
|
||||
* https://github.com/jinzhu/gorm/: SQL orm
|
||||
* https://github.com/go-gormigrate/gormigrate: Database migration system
|
||||
* Built-in shell
|
||||
* https://github.com/olekukonko/tablewriter: Ascii tables
|
||||
* https://github.com/asaskevich/govalidator: Valide user inputs
|
||||
* https://github.com/dustin/go-humanize: Human-friendly representation of technical data (time ago, bytes, ...)
|
||||
* https://github.com/mgutz/ansi: Terminal color helpers
|
||||
* https://github.com/urfave/cli: CLI flag parsing with subcommands support
|
||||
|
||||

|
||||
|
||||
## Contributors
|
||||
|
||||
### Code Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
|
||||
<a href="https://github.com/moul/sshportal/graphs/contributors"><img src="https://opencollective.com/sshportal/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
### Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/sshportal/contribute)]
|
||||
|
||||
#### Individuals
|
||||
|
||||
<a href="https://opencollective.com/sshportal"><img src="https://opencollective.com/sshportal/individuals.svg?width=890"></a>
|
||||
|
||||
#### Organizations
|
||||
|
||||
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/sshportal/contribute)]
|
||||
|
||||
<a href="https://opencollective.com/sshportal/organization/0/website"><img src="https://opencollective.com/sshportal/organization/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/1/website"><img src="https://opencollective.com/sshportal/organization/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/2/website"><img src="https://opencollective.com/sshportal/organization/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/3/website"><img src="https://opencollective.com/sshportal/organization/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/4/website"><img src="https://opencollective.com/sshportal/organization/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/5/website"><img src="https://opencollective.com/sshportal/organization/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/6/website"><img src="https://opencollective.com/sshportal/organization/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/7/website"><img src="https://opencollective.com/sshportal/organization/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/8/website"><img src="https://opencollective.com/sshportal/organization/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/9/website"><img src="https://opencollective.com/sshportal/organization/9/avatar.svg"></a>
|
||||
|
||||
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-slate
|
||||
40
acl.go
@@ -1,40 +0,0 @@
|
||||
package main
|
||||
|
||||
import "sort"
|
||||
|
||||
type ByWeight []ACL
|
||||
|
||||
func (a ByWeight) Len() int { return len(a) }
|
||||
func (a ByWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByWeight) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
|
||||
|
||||
func CheckACLs(user User, host Host) (string, error) {
|
||||
// shared ACLs between user and host
|
||||
aclMap := map[uint]ACL{}
|
||||
for _, userGroup := range user.Groups {
|
||||
for _, userGroupACL := range userGroup.ACLs {
|
||||
for _, hostGroup := range host.Groups {
|
||||
for _, hostGroupACL := range hostGroup.ACLs {
|
||||
if userGroupACL.ID == hostGroupACL.ID {
|
||||
aclMap[userGroupACL.ID] = userGroupACL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// FIXME: add ACLs that match host pattern
|
||||
|
||||
// deny by default if no shared ACL
|
||||
if len(aclMap) == 0 {
|
||||
return "deny", nil // default action
|
||||
}
|
||||
|
||||
// transofrm map to slice and sort it
|
||||
acls := []ACL{}
|
||||
for _, acl := range aclMap {
|
||||
acls = append(acls, acl)
|
||||
}
|
||||
sort.Sort(ByWeight(acls))
|
||||
|
||||
return acls[0].Action, nil
|
||||
}
|
||||
43
acl_test.go
@@ -1,43 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestCheckACLs(t *testing.T) {
|
||||
Convey("Testing CheckACLs", t, func() {
|
||||
// create tmp dir
|
||||
tempDir, err := ioutil.TempDir("", "sshportal")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// create sqlite db
|
||||
db, err := gorm.Open("sqlite3", filepath.Join(tempDir, "sshportal.db"))
|
||||
db.LogMode(false)
|
||||
So(dbInit(db), ShouldBeNil)
|
||||
|
||||
// create dummy objects
|
||||
hostGroup, err := FindHostGroupByIdOrName(db, "default")
|
||||
So(err, ShouldBeNil)
|
||||
db.Create(&Host{Groups: []HostGroup{*hostGroup}})
|
||||
|
||||
//. load db
|
||||
var (
|
||||
hosts []Host
|
||||
users []User
|
||||
)
|
||||
db.Preload("Groups").Preload("Groups.ACLs").Find(&hosts)
|
||||
db.Preload("Groups").Preload("Groups.ACLs").Find(&users)
|
||||
|
||||
// test
|
||||
action, err := CheckACLs(users[0], hosts[0])
|
||||
So(err, ShouldBeNil)
|
||||
So(action, ShouldEqual, "allow")
|
||||
})
|
||||
}
|
||||
49
crypto.go
@@ -1,49 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func NewSSHKey(keyType string, length uint) (*SSHKey, error) {
|
||||
key := SSHKey{
|
||||
Type: keyType,
|
||||
Length: length,
|
||||
}
|
||||
|
||||
// generate the private key
|
||||
if keyType != "rsa" {
|
||||
return nil, fmt.Errorf("key type not supported: %q", key.Type)
|
||||
}
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// convert priv key to x509 format
|
||||
var pemKey = &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||
}
|
||||
buf := bytes.NewBufferString("")
|
||||
if err = pem.Encode(buf, pemKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key.PrivKey = buf.String()
|
||||
|
||||
// generte authorized-key formatted pubkey output
|
||||
pub, err := gossh.NewPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key.PubKey = strings.TrimSpace(string(gossh.MarshalAuthorizedKey(pub)))
|
||||
|
||||
return &key, nil
|
||||
}
|
||||
424
db.go
@@ -1,424 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
SSHKeys []SSHKey `json:"keys"`
|
||||
Hosts []Host `json:"hosts"`
|
||||
UserKeys []UserKey `json:"user_keys"`
|
||||
Users []User `json:"users"`
|
||||
UserGroups []UserGroup `json:"user_groups"`
|
||||
HostGroups []HostGroup `json:"host_groups"`
|
||||
ACLs []ACL `json:"acls"`
|
||||
Date time.Time `json:"date"`
|
||||
}
|
||||
|
||||
type SSHKey struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string // FIXME: govalidator: min length 3, alphanum
|
||||
Type string
|
||||
Length uint
|
||||
Fingerprint string
|
||||
PrivKey string `sql:"size:10000;"`
|
||||
PubKey string `sql:"size:10000;"`
|
||||
Hosts []Host
|
||||
Comment string
|
||||
}
|
||||
|
||||
type Host struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `gorm:"size:63"` // FIXME: govalidator: min length 3, alphanum
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
SSHKey *SSHKey
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
Groups []HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Fingerprint string // FIXME: replace with hostkey ?
|
||||
Comment string
|
||||
}
|
||||
|
||||
type UserKey struct {
|
||||
gorm.Model
|
||||
Key []byte `sql:"size:10000;"`
|
||||
UserID uint
|
||||
User *User
|
||||
Comment string
|
||||
}
|
||||
|
||||
type User struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
IsAdmin bool
|
||||
Email string // FIXME: govalidator: email
|
||||
Name string // FIXME: govalidator: min length 3, alphanum
|
||||
Keys []UserKey
|
||||
Groups []UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string
|
||||
InviteToken string
|
||||
}
|
||||
|
||||
type UserGroup struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Users []User `gorm:"many2many:user_user_groups;"`
|
||||
ACLs []ACL `gorm:"many2many:user_group_acls;"`
|
||||
Comment string
|
||||
}
|
||||
|
||||
type HostGroup struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Hosts []Host `gorm:"many2many:host_host_groups;"`
|
||||
ACLs []ACL `gorm:"many2many:host_group_acls;"`
|
||||
Comment string
|
||||
}
|
||||
|
||||
type ACL struct {
|
||||
gorm.Model
|
||||
HostGroups []HostGroup `gorm:"many2many:host_group_acls;"`
|
||||
UserGroups []UserGroup `gorm:"many2many:user_group_acls;"`
|
||||
HostPattern string
|
||||
Action string
|
||||
Weight uint
|
||||
Comment string
|
||||
}
|
||||
|
||||
func dbInit(db *gorm.DB) error {
|
||||
db.AutoMigrate(&User{})
|
||||
db.AutoMigrate(&SSHKey{})
|
||||
db.AutoMigrate(&Host{})
|
||||
db.AutoMigrate(&UserKey{})
|
||||
db.AutoMigrate(&UserGroup{})
|
||||
db.AutoMigrate(&HostGroup{})
|
||||
db.AutoMigrate(&ACL{})
|
||||
// FIXME: check if indexes exist to avoid gorm warns
|
||||
db.Exec(`CREATE UNIQUE INDEX uix_keys_name ON "ssh_keys"("name") WHERE ("deleted_at" IS NULL)`)
|
||||
db.Exec(`CREATE UNIQUE INDEX uix_hosts_name ON "hosts"("name") WHERE ("deleted_at" IS NULL)`)
|
||||
db.Exec(`CREATE UNIQUE INDEX uix_users_name ON "users"("email") WHERE ("deleted_at" IS NULL)`)
|
||||
db.Exec(`CREATE UNIQUE INDEX uix_usergroups_name ON "user_groups"("name") WHERE ("deleted_at" IS NULL)`)
|
||||
db.Exec(`CREATE UNIQUE INDEX uix_hostgroups_name ON "host_groups"("name") WHERE ("deleted_at" IS NULL)`)
|
||||
|
||||
// create default ssh key
|
||||
var count uint
|
||||
if err := db.Table("ssh_keys").Where("name = ?", "default").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
key, err := NewSSHKey("rsa", 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.Name = "default"
|
||||
key.Comment = "created by sshportal"
|
||||
if err := db.Create(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create default host group
|
||||
if err := db.Table("host_groups").Where("name = ?", "default").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
hostGroup := HostGroup{
|
||||
Name: "default",
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
if err := db.Create(&hostGroup).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create default user group
|
||||
if err := db.Table("user_groups").Where("name = ?", "default").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
userGroup := UserGroup{
|
||||
Name: "default",
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
if err := db.Create(&userGroup).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create default acl
|
||||
if err := db.Table("acls").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
var defaultUserGroup UserGroup
|
||||
db.Where("name = ?", "default").First(&defaultUserGroup)
|
||||
var defaultHostGroup HostGroup
|
||||
db.Where("name = ?", "default").First(&defaultHostGroup)
|
||||
acl := ACL{
|
||||
UserGroups: []UserGroup{defaultUserGroup},
|
||||
HostGroups: []HostGroup{defaultHostGroup},
|
||||
Action: "allow",
|
||||
//HostPattern: "",
|
||||
//Weight: 0,
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
if err := db.Create(&acl).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create admin user
|
||||
var defaultUserGroup UserGroup
|
||||
db.Where("name = ?", "default").First(&defaultUserGroup)
|
||||
db.Table("users").Count(&count)
|
||||
if count == 0 {
|
||||
// if no admin, create an account for the first connection
|
||||
user := User{
|
||||
Name: "Administrator",
|
||||
Email: "admin@sshportal",
|
||||
Comment: "created by sshportal",
|
||||
IsAdmin: true,
|
||||
InviteToken: RandStringBytes(16),
|
||||
Groups: []UserGroup{defaultUserGroup},
|
||||
}
|
||||
db.Create(&user)
|
||||
log.Printf("Admin user created, use the user 'invite:%s' to associate a public key with this account", user.InviteToken)
|
||||
}
|
||||
|
||||
// create host ssh key
|
||||
if err := db.Table("ssh_keys").Where("name = ?", "host").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
key, err := NewSSHKey("rsa", 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.Name = "host"
|
||||
key.Comment = "created by sshportal"
|
||||
if err := db.Create(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dbDemo(db *gorm.DB) error {
|
||||
hostGroup, err := FindHostGroupByIdOrName(db, "default")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := FindKeyByIdOrName(db, "default")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
host1 = Host{Name: "sdf", Addr: "sdf.org:22", User: "new", SSHKeyID: key.ID, Groups: []HostGroup{*hostGroup}}
|
||||
host2 = Host{Name: "whoami", Addr: "whoami.filippo.io:22", User: "test", SSHKeyID: key.ID, Groups: []HostGroup{*hostGroup}}
|
||||
host3 = Host{Name: "ssh-chat", Addr: "chat.shazow.net:22", User: "test", SSHKeyID: key.ID, Fingerprint: "MD5:e5:d5:d1:75:90:38:42:f6:c7:03:d7:d0:56:7d:6a:db", Groups: []HostGroup{*hostGroup}}
|
||||
)
|
||||
|
||||
// FIXME: check if hosts exist to avoid `UNIQUE constraint` error
|
||||
db.Create(&host1)
|
||||
db.Create(&host2)
|
||||
db.Create(&host3)
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoteHostFromSession(s ssh.Session, db *gorm.DB) (*Host, error) {
|
||||
var host Host
|
||||
db.Preload("SSHKey").Where("name = ?", s.User()).Find(&host)
|
||||
if host.Name == "" {
|
||||
// FIXME: add available hosts
|
||||
return nil, fmt.Errorf("No such target: %q", s.User())
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
func (host *Host) URL() string {
|
||||
return fmt.Sprintf("%s@%s", host.User, host.Addr)
|
||||
}
|
||||
|
||||
func NewHostFromURL(rawurl string) (*Host, error) {
|
||||
if !strings.Contains(rawurl, "://") {
|
||||
rawurl = "ssh://" + rawurl
|
||||
}
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host := Host{Addr: u.Host}
|
||||
if !strings.Contains(host.Addr, ":") {
|
||||
host.Addr += ":22" // add port if not present
|
||||
}
|
||||
host.User = "root" // default username
|
||||
if u.User != nil {
|
||||
password, _ := u.User.Password()
|
||||
host.Password = password
|
||||
host.User = u.User.Username()
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
func (host *Host) Hostname() string {
|
||||
return strings.Split(host.Addr, ":")[0]
|
||||
}
|
||||
|
||||
// Host helpers
|
||||
|
||||
func FindHostByIdOrName(db *gorm.DB, query string) (*Host, error) {
|
||||
var host Host
|
||||
if err := db.Preload("Groups").Preload("SSHKey").Where("id = ?", query).Or("name = ?", query).First(&host).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
func FindHostsByIdOrName(db *gorm.DB, queries []string) ([]*Host, error) {
|
||||
var hosts []*Host
|
||||
for _, query := range queries {
|
||||
host, err := FindHostByIdOrName(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
// SSHKey helpers
|
||||
|
||||
func FindKeyByIdOrName(db *gorm.DB, query string) (*SSHKey, error) {
|
||||
var key SSHKey
|
||||
if err := db.Preload("Hosts").Where("id = ?", query).Or("name = ?", query).First(&key).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &key, nil
|
||||
}
|
||||
func FindKeysByIdOrName(db *gorm.DB, queries []string) ([]*SSHKey, error) {
|
||||
var keys []*SSHKey
|
||||
for _, query := range queries {
|
||||
key, err := FindKeyByIdOrName(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// HostGroup helpers
|
||||
|
||||
func FindHostGroupByIdOrName(db *gorm.DB, query string) (*HostGroup, error) {
|
||||
var hostGroup HostGroup
|
||||
if err := db.Preload("ACLs").Preload("Hosts").Where("id = ?", query).Or("name = ?", query).First(&hostGroup).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &hostGroup, nil
|
||||
}
|
||||
func FindHostGroupsByIdOrName(db *gorm.DB, queries []string) ([]*HostGroup, error) {
|
||||
var hostGroups []*HostGroup
|
||||
for _, query := range queries {
|
||||
hostGroup, err := FindHostGroupByIdOrName(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hostGroups = append(hostGroups, hostGroup)
|
||||
}
|
||||
return hostGroups, nil
|
||||
}
|
||||
|
||||
// UserGroup heleprs
|
||||
|
||||
func FindUserGroupByIdOrName(db *gorm.DB, query string) (*UserGroup, error) {
|
||||
var userGroup UserGroup
|
||||
if err := db.Preload("ACLs").Preload("Users").Where("id = ?", query).Or("name = ?", query).First(&userGroup).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &userGroup, nil
|
||||
}
|
||||
func FindUserGroupsByIdOrName(db *gorm.DB, queries []string) ([]*UserGroup, error) {
|
||||
var userGroups []*UserGroup
|
||||
for _, query := range queries {
|
||||
userGroup, err := FindUserGroupByIdOrName(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userGroups = append(userGroups, userGroup)
|
||||
}
|
||||
return userGroups, nil
|
||||
}
|
||||
|
||||
// User helpers
|
||||
|
||||
func FindUserByIdOrEmail(db *gorm.DB, query string) (*User, error) {
|
||||
var user User
|
||||
if err := db.Preload("Groups").Preload("Keys").Where("id = ?", query).Or("email = ?", query).First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
func FindUsersByIdOrEmail(db *gorm.DB, queries []string) ([]*User, error) {
|
||||
var users []*User
|
||||
for _, query := range queries {
|
||||
user, err := FindUserByIdOrEmail(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// ACL helpers
|
||||
|
||||
func FindACLById(db *gorm.DB, query string) (*ACL, error) {
|
||||
var acl ACL
|
||||
if err := db.Preload("UserGroups").Preload("HostGroups").Where("id = ?", query).First(&acl).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &acl, nil
|
||||
}
|
||||
func FindACLsById(db *gorm.DB, queries []string) ([]*ACL, error) {
|
||||
var acls []*ACL
|
||||
for _, query := range queries {
|
||||
acl, err := FindACLById(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acls = append(acls, acl)
|
||||
}
|
||||
return acls, nil
|
||||
}
|
||||
|
||||
// UserKey helpers
|
||||
|
||||
func FindUserkeyById(db *gorm.DB, query string) (*UserKey, error) {
|
||||
var userkey UserKey
|
||||
if err := db.Preload("User").Where("id = ?", query).First(&userkey).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &userkey, nil
|
||||
}
|
||||
func FindUserkeysById(db *gorm.DB, queries []string) ([]*UserKey, error) {
|
||||
var userkeys []*UserKey
|
||||
for _, query := range queries {
|
||||
userkey, err := FindUserkeyById(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userkeys = append(userkeys, userkey)
|
||||
}
|
||||
return userkeys, nil
|
||||
}
|
||||
24
examples/homebrew/sshportal.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
require "language/go"
|
||||
|
||||
class Sshportal < Formula
|
||||
desc "sshportal: simple, fun and transparent SSH bastion"
|
||||
homepage "https://github.com/moul/sshportal"
|
||||
url "https://github.com/moul/sshportal/archive/v1.7.1.tar.gz"
|
||||
sha256 "4611ae2f30cc595b2fb789bd0c92550533db6d4b63c638dd78cf85517b6aeaf0"
|
||||
head "https://github.com/moul/sshportal.git"
|
||||
|
||||
depends_on "go" => :build
|
||||
|
||||
def install
|
||||
ENV["GOPATH"] = buildpath
|
||||
ENV["GOBIN"] = buildpath
|
||||
(buildpath/"src/github.com/moul/sshportal").install Dir["*"]
|
||||
|
||||
system "go", "build", "-o", "#{bin}/sshportal", "-v", "github.com/moul/sshportal"
|
||||
end
|
||||
|
||||
test do
|
||||
output = shell_output(bin/"sshportal --version")
|
||||
assert output.include? "sshportal version "
|
||||
end
|
||||
end
|
||||
4
examples/integration/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM occitech/ssh-client
|
||||
ENTRYPOINT ["/bin/sh", "-c"]
|
||||
CMD ["/integration/_client.sh"]
|
||||
COPY . /integration
|
||||
10
examples/integration/Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
run:
|
||||
docker-compose down
|
||||
docker-compose up -d sshportal
|
||||
docker-compose build client
|
||||
docker-compose exec sshportal /bin/sshportal healthcheck --wait --quiet
|
||||
docker-compose run client /integration/_client.sh
|
||||
docker-compose down
|
||||
|
||||
build:
|
||||
docker-compose build
|
||||
79
examples/integration/_client.sh
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
cp /integration/client_test_rsa ~/.ssh/id_rsa
|
||||
chmod -R 700 ~/.ssh
|
||||
cat >~/.ssh/config <<EOF
|
||||
Host sshportal
|
||||
Port 2222
|
||||
HostName sshportal
|
||||
|
||||
Host testserver
|
||||
Port 2222
|
||||
HostName testserver
|
||||
|
||||
Host *
|
||||
StrictHostKeyChecking no
|
||||
ControlMaster auto
|
||||
SendEnv TEST_*
|
||||
|
||||
EOF
|
||||
|
||||
set -x
|
||||
|
||||
# login
|
||||
ssh sshportal -l invite:integration
|
||||
|
||||
# hostgroup/usergroup/acl
|
||||
ssh sshportal -l admin hostgroup create
|
||||
ssh sshportal -l admin hostgroup create --name=hg1
|
||||
ssh sshportal -l admin hostgroup create --name=hg2 --comment=test
|
||||
ssh sshportal -l admin hostgroup inspect hg1 hg2
|
||||
ssh sshportal -l admin hostgroup ls
|
||||
|
||||
ssh sshportal -l admin usergroup create
|
||||
ssh sshportal -l admin usergroup create --name=ug1
|
||||
ssh sshportal -l admin usergroup create --name=ug2 --comment=test
|
||||
ssh sshportal -l admin usergroup inspect ug1 ug2
|
||||
ssh sshportal -l admin usergroup ls
|
||||
|
||||
ssh sshportal -l admin acl create --ug=ug1 --ug=ug2 --hg=hg1 --hg=hg2 --comment=test --action=allow --weight=42
|
||||
ssh sshportal -l admin acl inspect 2
|
||||
ssh sshportal -l admin acl ls
|
||||
|
||||
# basic host create
|
||||
ssh sshportal -l admin host create bob@example.org:1234
|
||||
ssh sshportal -l admin host create test42
|
||||
ssh sshportal -l admin host create --name=testtest --comment=test --password=test test@test.test
|
||||
ssh sshportal -l admin host create --group=hg1 --group=hg2 hostwithgroups.org
|
||||
ssh sshportal -l admin host inspect example test42 testtest hostwithgroups
|
||||
ssh sshportal -l admin host update --assign-group=hg1 test42
|
||||
ssh sshportal -l admin host update --unassign-group=hg1 test42
|
||||
ssh sshportal -l admin host update --assign-group=hg1 test42
|
||||
ssh sshportal -l admin host update --assign-group=hg2 --unassign-group=hg2 test42
|
||||
ssh sshportal -l admin host ls
|
||||
|
||||
# backup/restore
|
||||
ssh sshportal -l admin config backup --indent --ignore-events > backup-1
|
||||
ssh sshportal -l admin config restore --confirm < backup-1
|
||||
ssh sshportal -l admin config backup --indent --ignore-events > backup-2
|
||||
(
|
||||
cat backup-1 | grep -v '"date":' | grep -v 'tedAt":' > backup-1.clean
|
||||
cat backup-2 | grep -v '"date":' | grep -v 'tedAt":' > backup-2.clean
|
||||
set -xe
|
||||
diff backup-1.clean backup-2.clean
|
||||
)
|
||||
|
||||
if [ "$CIRCLECI" = "true" ]; then
|
||||
echo "Strage behavior with cross-container communication on CircleCI, skipping some tests..."
|
||||
else
|
||||
# bastion
|
||||
ssh sshportal -l admin host create --name=testserver toto@testserver:2222
|
||||
out="$(ssh sshportal -l testserver echo hello | head -n 1)"
|
||||
test "$out" = '{"User":"toto","Environ":null,"Command":["echo","hello"]}'
|
||||
|
||||
out="$(TEST_A=1 TEST_B=2 TEST_C=3 TEST_D=4 TEST_E=5 TEST_F=6 TEST_G=7 TEST_H=8 TEST_I=9 ssh sshportal -l testserver echo hello | head -n 1)"
|
||||
test "$out" = '{"User":"toto","Environ":["TEST_A=1","TEST_B=2","TEST_C=3","TEST_D=4","TEST_E=5","TEST_F=6","TEST_G=7","TEST_H=8","TEST_I=9"],"Command":["echo","hello"]}'
|
||||
fi
|
||||
|
||||
# TODO: test more cases (forwards, scp, sftp, interactive, pty, stdin, exit code, ...)
|
||||
27
examples/integration/client_test_rsa
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEAxV0ds/oMuOw9QVLFgxaM0Js2IdJKiYLnmKq96IuZ/wMqMea3
|
||||
qi1UfNBPUQ2CojwbJGTea8cA9J1Et+a6v1mL66YG8zyxmhdlKHm2KOMnUXSfWPNg
|
||||
ZArXH7Uj4Nx1k/O1ujfQFAsYTx63kMqwq1lM9JrExLSdp/8D/zQAyF68c82w8UZH
|
||||
aIpLZJkM/fgh0VJWiw65NYAzuIkJNBgZR8rEBQU7V3lCqFGcSJ88MoqIdVGy0I4b
|
||||
GGpO9VppDTf+uYGYDthhXlV0nHM45neWL5hzFK6oqbLFLpsaUOY7C3kKv+8+B3lX
|
||||
p3OfGVoFy7u3evro+yRQEMQ+myS5UBIHaI3qOwIDAQABAoIBABM7/vASV3kSNOoP
|
||||
2gXrha+y4LStHOyH4HBFe5qVOF3c/hi85ntkTY6YcpJwoaGUAAUs+2w/ib1NMmxF
|
||||
xT9ux68gkB7WdGyTCR3HttQHR0at+fWeSm+Vit+hNKzub1sK7lQGqnW5mxXi5Xrr
|
||||
9gnM+y3/g1u0SoUb2lTdyZG9gdo7LnLElzRinraEqTJUowXkqzAhGf1A+Kgp2fkb
|
||||
/+QP1oiK8QeOFOsITD2UwIVCBRwRl5TjjwfLQ4El6oAWNjcL1ZfSmQLiXZ7U8Smk
|
||||
Cd+BI+6ZDLA43fBUGDjbg4+2dt2JoKNkS0FfqhCW+Z2A0+ClJ8pwuMqRz8XXaOYr
|
||||
ONCqOPECgYEA/qyWxSUjEWMvN3tC/mZPEbwHP3m7mbR1KGwhZylWVCmEF7kVC6il
|
||||
/ICQZUI9ekyGJZ/SKZKwxDe7oeV+vFsus/9FWC5wrp45Xm4kEUwsBr4bWvuNpVOq
|
||||
jrKecY8NgPZS1X6Uc5BbpiE9/VF2gCdYVVCDXP1NfO2MDhkniXJQUEMCgYEAxmQl
|
||||
3s/vih9rXllPZcWHafjnFcGU1AIiJD1c+8lAqwCZzm0Bt0Ex4s1t3lp0ew6YBVXN
|
||||
yGy+BORxOC9FQGTlKZNk/S705+8iAVNc9Sy7XbgN3GY3eat7XYbNpGbQrjiyZ+7I
|
||||
pdEnoHWQD4NFXHaVsXaVHcBFUovXKoes2PODeqkCgYEAoN/3Ucv2zgoAjqSfmkKY
|
||||
mhRT48YLOroi9AjyRM95CCs9lRrGb5n2WH4COOTSHwpuByBhSv+uCBVIwqlNGMDk
|
||||
zLFpZZ3YcoXiqYMb541dlljKwPt8673hVMkCi6uZFSkFBHY0YpgDPPtsxDOMjsHL
|
||||
7ACzKq+cHlmUimdbcViz4S8CgYEAr2+sVYaHixsRtVNA9PxiLQIgR4rx8zEXw/hH
|
||||
m5hyiUV0vaiDlewfEzMab0CKNK/JGx6vZQdUWbsxq7+Re8o9JDDlY0b854T+CzIO
|
||||
x/iQj+XMzBPQBtXvt9sXSsRo0Uft7B6qbIeyhSCxDibFVWjAIzh70N1P8BkdYsyr
|
||||
uwZMRFECgYEA5QuutlFLI7hMPdBQvsEhjdVwKAj7LvpNemgxDpEoMiQWWm51XzcP
|
||||
IZjlCwl1UvIE0MxowtvNr5lQuGRN8/88Dajpq+W6eeTSCKi67nn0VZh13cQLKvoX
|
||||
DRZ6nfC3iLnEYKK+KN/I3NY7JcSjHmW6V8WtrCYAi2D5Ns05XJAG6t8=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
27
examples/integration/docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
version: '3.0'
|
||||
|
||||
services:
|
||||
sshportal:
|
||||
image: moul/sshportal
|
||||
environment:
|
||||
- SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN=integration
|
||||
command: server --debug
|
||||
depends_on:
|
||||
- testserver
|
||||
ports:
|
||||
- 2222
|
||||
|
||||
testserver:
|
||||
image: moul/sshportal
|
||||
command: _test_server
|
||||
ports:
|
||||
- 2222
|
||||
|
||||
client:
|
||||
build: .
|
||||
depends_on:
|
||||
- sshportal
|
||||
- testserver
|
||||
#volumes:
|
||||
# - .:/integration
|
||||
tty: true
|
||||
@@ -4,12 +4,16 @@ services:
|
||||
sshportal:
|
||||
build: ../..
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SSHPORTAL_DB_DRIVER: mysql
|
||||
SSHPORTAL_DATABASE_URL: "root:root@tcp(mysql:3306)/db?charset=utf8&parseTime=true&loc=Local"
|
||||
SSHPORTAL_DEBUG: 1
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
links:
|
||||
- mysql
|
||||
command: --db-driver=mysql --debug --db-conn="root:root@tcp(mysql:3306)/db?charset=utf8&parseTime=true&loc=Local"
|
||||
command: server
|
||||
ports:
|
||||
- 2222:2222
|
||||
|
||||
|
||||
35
go.mod
Normal file
@@ -0,0 +1,35 @@
|
||||
module moul.io/sshportal
|
||||
|
||||
require (
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/creack/pty v1.1.9 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/docker v1.13.1
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/gliderlabs/ssh v0.2.2
|
||||
github.com/go-sql-driver/mysql v1.5.0 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||
github.com/jinzhu/gorm v1.9.12
|
||||
github.com/kr/pty v1.1.8
|
||||
github.com/mattn/go-colorable v0.1.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.8 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/reiver/go-oi v1.0.0
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
|
||||
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4-0.20190330032615-68dc04aab96a
|
||||
github.com/urfave/cli v1.22.2
|
||||
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867 // indirect
|
||||
gopkg.in/gormigrate.v1 v1.6.0
|
||||
moul.io/srand v1.4.0
|
||||
)
|
||||
|
||||
go 1.14
|
||||
129
go.sum
Normal file
@@ -0,0 +1,129 @@
|
||||
cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
|
||||
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/jinzhu/gorm v1.9.2 h1:lCvgEaqe/HVE+tjAR2mt4HbbHAZsQOv3XAZiEZV37iw=
|
||||
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
||||
github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
|
||||
github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3 h1:xvj06l8iSwiWpYgm8MbPp+naBg+pwfqmdXabzqPCn/8=
|
||||
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
|
||||
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
|
||||
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
|
||||
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
|
||||
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e h1:quuzZLi72kkJjl+f5AQ93FMcadG19WkS7MO6TXFOSas=
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvEWwEIx86DB9Ke/+a5wBI464eDRo3eF0LcfpWg=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3 h1:yxUGvEatvDMO6gkhwx82Va+Czdyui9LiCw6a5YB/2f8=
|
||||
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3/go.mod h1:1Q04m7wmv/IMoZU9t8UkH+n9McWn4i3H9v9LnMgqloo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 h1:hBSHahWMEgzwRyS6dRpxY0XyjZsHyQ61s084wo5PJe0=
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4-0.20190330032615-68dc04aab96a h1:XmieTxr5Ejfoo1izsMZO4qWqOTpYagCqNMJyP87ONS0=
|
||||
github.com/smartystreets/goconvey v1.6.4-0.20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg=
|
||||
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867 h1:JoRuNIf+rpHl+VhScRQQvzbHed86tKkqwPMV34T8myw=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/gormigrate.v1 v1.6.0 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI=
|
||||
gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
moul.io/srand v1.4.0 h1:r5ZMiWDN0ni0lTV7KzJR/jx0K7GivJYW5WaXmufgeik=
|
||||
moul.io/srand v1.4.0/go.mod h1:P2uaZB+GFstFNo8sEj6/U8FRV1n25kD0LLckFpJ+qvc=
|
||||
73
healthcheck.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// perform a healthcheck test without requiring an ssh client or an ssh key (used for Docker's HEALTHCHECK)
|
||||
func healthcheck(addr string, wait, quiet bool) error {
|
||||
cfg := gossh.ClientConfig{
|
||||
User: "healthcheck",
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key gossh.PublicKey) error { return nil },
|
||||
Auth: []gossh.AuthMethod{gossh.Password("healthcheck")},
|
||||
}
|
||||
|
||||
if wait {
|
||||
for {
|
||||
if err := healthcheckOnce(addr, cfg, quiet); err != nil {
|
||||
if !quiet {
|
||||
log.Printf("error: %v", err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := healthcheckOnce(addr, cfg, quiet); err != nil {
|
||||
if quiet {
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func healthcheckOnce(addr string, config gossh.ClientConfig, quiet bool) error {
|
||||
client, err := gossh.Dial("tcp", addr, &config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := session.Close(); err != nil {
|
||||
if !quiet {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var b bytes.Buffer
|
||||
session.Stdout = &b
|
||||
if err := session.Run(""); err != nil {
|
||||
return err
|
||||
}
|
||||
stdout := strings.TrimSpace(b.String())
|
||||
if stdout != "OK" {
|
||||
return fmt.Errorf("invalid stdout: %q expected 'OK'", stdout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
22
helm/sshportal/.helmignore
Normal file
@@ -0,0 +1,22 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
21
helm/sshportal/Chart.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
apiVersion: v2
|
||||
name: sshportal
|
||||
description: A Helm chart for SSHPortal on Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application.
|
||||
appVersion: 1.10.0
|
||||
33
helm/sshportal/templates/NOTES.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
1. Get the admin invitation token (only on first install):
|
||||
export INVITE=$(kubectl --namespace sshportal logs -l "app.kubernetes.io/name={{ include "sshportal.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" | grep -Eo "invite:[a-zA-Z0-9]+")
|
||||
|
||||
2. Get the service IP and Port:
|
||||
{{- if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sshportal.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sshportal.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sshportal.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sshportal.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 2222:{{ .Values.service.port }}
|
||||
{{- end }}
|
||||
|
||||
3. Enroll your SSH public key:
|
||||
{{- if contains "NodePort" .Values.service.type }}
|
||||
ssh $NODE_IP -p $NODE_PORT -l $INVITE
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
ssh $SERVICE_IP -p {{ .Values.service.port }} -l $INVITE
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
ssh localhost -p 2222 -l $INVITE
|
||||
{{- end }}
|
||||
|
||||
4. Configure your {{ include "sshportal.name" . }} install:
|
||||
{{- if contains "NodePort" .Values.service.type }}
|
||||
ssh admin@$NODE_IP -p $NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
ssh admin@$SERVICE_IP -p {{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
ssh admin@localhost -p 2222
|
||||
{{- end }}
|
||||
63
helm/sshportal/templates/_helpers.tpl
Normal file
@@ -0,0 +1,63 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "sshportal.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "sshportal.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "sshportal.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "sshportal.labels" -}}
|
||||
helm.sh/chart: {{ include "sshportal.chart" . }}
|
||||
{{ include "sshportal.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "sshportal.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "sshportal.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "sshportal.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
{{ default (include "sshportal.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else -}}
|
||||
{{ default "default" .Values.serviceAccount.name }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
69
helm/sshportal/templates/deployment.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "sshportal.fullname" . }}
|
||||
labels:
|
||||
{{- include "sshportal.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "sshportal.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "sshportal.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:v{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: ssh
|
||||
containerPort: 2222
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sshportal
|
||||
- healthcheck
|
||||
- --quiet
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sshportal
|
||||
- healthcheck
|
||||
- --quiet
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.mysql.enabled }}
|
||||
- name: SSHPORTAL_DATABASE_URL
|
||||
value: {{ .Values.mysql.user }}:{{ .Values.mysql.password }}@tcp({{ .Values.mysql.server }}:{{ .Values.mysql.port }})/{{ .Values.mysql.database }}?charset=utf8&parseTime=true&loc=Local
|
||||
- name: SSHPORTAL_DB_DRIVER
|
||||
value: mysql
|
||||
{{- end }}
|
||||
{{- if .Values.debug}}
|
||||
- name: SSHPORTAL_DEBUG
|
||||
value: "1"
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
21
helm/sshportal/templates/horizontal-pod-autoscaling.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
{{- if .Values.mysql.enabled }}
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "sshportal.fullname" . }}
|
||||
labels:
|
||||
{{- include "sshportal.labels" . | nindent 4 }}
|
||||
spec:
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "sshportal.fullname" . }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: {{ .Values.autoscaling.cpuTarget }}
|
||||
{{- end }}
|
||||
|
||||
17
helm/sshportal/templates/service.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "sshportal.fullname" . }}
|
||||
annotations:
|
||||
{{- toYaml .Values.service.annotations | nindent 4 }}
|
||||
labels:
|
||||
{{- include "sshportal.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: 2222
|
||||
protocol: TCP
|
||||
name: ssh
|
||||
selector:
|
||||
{{- include "sshportal.selectorLabels" . | nindent 4 }}
|
||||
15
helm/sshportal/templates/tests/test-connection.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "sshportal.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{ include "sshportal.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test-success
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "sshportal.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
119
helm/sshportal/values.yaml
Normal file
@@ -0,0 +1,119 @@
|
||||
# Default values for sshportal.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
## Enable SSHPortal debug mode
|
||||
##
|
||||
debug: false
|
||||
|
||||
## SSH Portal Docker image
|
||||
##
|
||||
image:
|
||||
repository: moul/sshportal
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
## Reference to one or more secrets to be used when pulling images
|
||||
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
##
|
||||
imagePullSecrets: []
|
||||
|
||||
## Provide a name in place of sshportal for `app:` labels
|
||||
##
|
||||
nameOverride: ""
|
||||
|
||||
## Provide a name to substitute for the full names of resources
|
||||
##
|
||||
fullnameOverride: ""
|
||||
|
||||
## PodSecurityContext holds pod-level security attributes.
|
||||
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
|
||||
##
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
## SecurityContext holds container-level security attributes.
|
||||
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
|
||||
##
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
## Service
|
||||
##
|
||||
service:
|
||||
## Configure additional annotations for SSHPortal service
|
||||
##
|
||||
annotations: {}
|
||||
# service.beta.kubernetes.io/openstack-internal-load-balancer: "true"
|
||||
|
||||
## Service type, one of
|
||||
## NodePort, ClusterIP, LoadBalancer
|
||||
##
|
||||
type: LoadBalancer
|
||||
|
||||
## Port to expose on the service
|
||||
##
|
||||
port: 22
|
||||
|
||||
## Define resources requests and limits
|
||||
## ref: https://kubernetes.io/docs/user-guide/compute-resources/
|
||||
##
|
||||
resources: {}
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# limits:
|
||||
# cpu: 2
|
||||
# memory: 2Gi
|
||||
|
||||
## Mysql/MariaDB configuration for HA
|
||||
##
|
||||
mysql:
|
||||
enabled: false
|
||||
|
||||
## Database user
|
||||
##
|
||||
user: sshportal
|
||||
|
||||
## Database password
|
||||
##
|
||||
password: change_me
|
||||
|
||||
## Database name
|
||||
##
|
||||
database: sshportal
|
||||
|
||||
## Database server FQDN or IP
|
||||
##
|
||||
server: mariadb-mariadb-galera
|
||||
|
||||
## Database port
|
||||
##
|
||||
port: 3306
|
||||
|
||||
## Define which Nodes the Pods are scheduled on.
|
||||
## ref: https://kubernetes.io/docs/user-guide/node-selection/
|
||||
##
|
||||
nodeSelector: {}
|
||||
|
||||
## The pod's tolerations.
|
||||
## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
|
||||
##
|
||||
tolerations: []
|
||||
|
||||
## Assign custom affinity rules
|
||||
## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/
|
||||
##
|
||||
affinity: {}
|
||||
|
||||
## HPA support, require `mysql.enable: true`
|
||||
## This section enables sshportal to autoscale based on metrics.
|
||||
##
|
||||
autoscaling:
|
||||
maxReplicas: 4
|
||||
minReplicas: 2
|
||||
cpuTarget: 60
|
||||
303
main.go
@@ -1,239 +1,116 @@
|
||||
package main
|
||||
package main // import "moul.io/sshportal"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/urfave/cli"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"moul.io/srand"
|
||||
)
|
||||
|
||||
var (
|
||||
// VERSION should be updated by hand at each release
|
||||
VERSION = "1.1.0"
|
||||
// GIT_TAG will be overwritten automatically by the build system
|
||||
GIT_TAG string
|
||||
// GIT_SHA will be overwritten automatically by the build system
|
||||
GIT_SHA string
|
||||
// GIT_BRANCH will be overwritten automatically by the build system
|
||||
GIT_BRANCH string
|
||||
)
|
||||
|
||||
type sshportalContextKey string
|
||||
|
||||
var (
|
||||
userContextKey = sshportalContextKey("user")
|
||||
messageContextKey = sshportalContextKey("message")
|
||||
errorContextKey = sshportalContextKey("error")
|
||||
// Version should be updated by hand at each release
|
||||
Version = "1.10.0+dev"
|
||||
// GitTag will be overwritten automatically by the build system
|
||||
GitTag string
|
||||
// GitSha will be overwritten automatically by the build system
|
||||
GitSha string
|
||||
// GitBranch will be overwritten automatically by the build system
|
||||
GitBranch string
|
||||
)
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rand.Seed(srand.Secure())
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = path.Base(os.Args[0])
|
||||
app.Author = "Manfred Touron"
|
||||
app.Version = VERSION + " (" + GIT_SHA + ")"
|
||||
app.Email = "https://github.com/moul/sshportal"
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "bind-address, b",
|
||||
EnvVar: "SSHPORTAL_BIND",
|
||||
Value: ":2222",
|
||||
Usage: "SSH server bind address",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "demo",
|
||||
Usage: "*unsafe* - demo mode: accept all connections",
|
||||
},
|
||||
/*cli.StringFlag{
|
||||
Name: "db-driver",
|
||||
Value: "sqlite3",
|
||||
Usage: "GORM driver (sqlite3)",
|
||||
},*/
|
||||
cli.StringFlag{
|
||||
Name: "db-conn",
|
||||
Value: "./sshportal.db",
|
||||
Usage: "GORM connection string",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug, D",
|
||||
Usage: "Display debug information",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "config-user",
|
||||
Usage: "SSH user that spawns a configuration shell",
|
||||
Value: "admin",
|
||||
app.Version = Version + " (" + GitSha + ")"
|
||||
app.Email = "https://moul.io/sshportal"
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "server",
|
||||
Usage: "Start sshportal server",
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := ensureLogDirectory(c.String("logs-location")); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := parseServerConfig(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return server(cfg)
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "bind-address, b",
|
||||
EnvVar: "SSHPORTAL_BIND",
|
||||
Value: ":2222",
|
||||
Usage: "SSH server bind address",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "db-driver",
|
||||
EnvVar: "SSHPORTAL_DB_DRIVER",
|
||||
Value: "sqlite3",
|
||||
Usage: "GORM driver (sqlite3)",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "db-conn",
|
||||
EnvVar: "SSHPORTAL_DATABASE_URL",
|
||||
Value: "./sshportal.db",
|
||||
Usage: "GORM connection string",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug, D",
|
||||
EnvVar: "SSHPORTAL_DEBUG",
|
||||
Usage: "Display debug information",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "aes-key",
|
||||
EnvVar: "SSHPORTAL_AES_KEY",
|
||||
Usage: "Encrypt sensitive data in database (length: 16, 24 or 32)",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "logs-location",
|
||||
EnvVar: "SSHPORTAL_LOGS_LOCATION",
|
||||
Value: "./log",
|
||||
Usage: "Store user session files",
|
||||
},
|
||||
cli.DurationFlag{
|
||||
Name: "idle-timeout",
|
||||
Value: 0,
|
||||
Usage: "Duration before an inactive connection is timed out (0 to disable)",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: "healthcheck",
|
||||
Action: func(c *cli.Context) error { return healthcheck(c.String("addr"), c.Bool("wait"), c.Bool("quiet")) },
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "addr, a",
|
||||
Value: "localhost:2222",
|
||||
Usage: "sshportal server address",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "wait, w",
|
||||
Usage: "Loop indefinitely until sshportal is ready",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "quiet, q",
|
||||
Usage: "Do not print errors, if any",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: "_test_server",
|
||||
Hidden: true,
|
||||
Action: testServer,
|
||||
},
|
||||
}
|
||||
app.Action = server
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Fatalf("error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func server(c *cli.Context) error {
|
||||
db, err := gorm.Open("sqlite3", c.String("db-conn"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if c.Bool("debug") {
|
||||
db.LogMode(true)
|
||||
}
|
||||
if err := dbInit(db); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ssh.Handle(func(s ssh.Session) {
|
||||
currentUser := s.Context().Value(userContextKey).(User)
|
||||
log.Printf("New connection: sshUser=%q remote=%q local=%q command=%q dbUser=id:%q,email:%s", s.User(), s.RemoteAddr(), s.LocalAddr(), s.Command(), currentUser.ID, currentUser.Email)
|
||||
|
||||
if err := s.Context().Value(errorContextKey); err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if msg := s.Context().Value(messageContextKey); msg != nil {
|
||||
fmt.Fprint(s, msg.(string))
|
||||
}
|
||||
|
||||
switch username := s.User(); {
|
||||
case username == c.String("config-user"):
|
||||
if !currentUser.IsAdmin {
|
||||
fmt.Fprintf(s, "You are not an administrator, permission denied.\n")
|
||||
return
|
||||
}
|
||||
if err := shell(c, s, s.Command(), db); err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
}
|
||||
case strings.HasPrefix(username, "invite:"):
|
||||
return
|
||||
default:
|
||||
host, err := RemoteHostFromSession(s, db)
|
||||
if err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
// FIXME: print available hosts
|
||||
return
|
||||
}
|
||||
|
||||
// load up-to-date objects
|
||||
// FIXME: cache them or try not to load them
|
||||
var tmpUser User
|
||||
if err := db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", currentUser.ID).First(&tmpUser).Error; err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
return
|
||||
}
|
||||
var tmpHost Host
|
||||
if err := db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", host.ID).First(&tmpHost).Error; err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
action, err := CheckACLs(tmpUser, tmpHost)
|
||||
if err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "allow":
|
||||
if err := proxy(s, host); err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
}
|
||||
case "deny":
|
||||
fmt.Fprintf(s, "You don't have permission to that host.\n")
|
||||
default:
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
opts := []ssh.Option{}
|
||||
if c.Bool("demo") {
|
||||
if c.Bool("demo") {
|
||||
if err := dbDemo(db); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opts = append(opts, ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
var (
|
||||
userKey UserKey
|
||||
user User
|
||||
username = ctx.User()
|
||||
)
|
||||
|
||||
// lookup user by key
|
||||
db.Where("key = ?", key.Marshal()).First(&userKey)
|
||||
if userKey.UserID > 0 {
|
||||
db.Where("id = ?", userKey.UserID).First(&user)
|
||||
if strings.HasPrefix(username, "invite:") {
|
||||
ctx.SetValue(errorContextKey, fmt.Errorf("invites are only supported for ney SSH keys; your ssh key is already associated with the user %q.", user.Email))
|
||||
}
|
||||
ctx.SetValue(userContextKey, user)
|
||||
return true
|
||||
}
|
||||
|
||||
// handle invite "links"
|
||||
if strings.HasPrefix(username, "invite:") {
|
||||
inputToken := strings.Split(username, ":")[1]
|
||||
if len(inputToken) == 16 {
|
||||
db.Where("invite_token = ?", inputToken).First(&user)
|
||||
}
|
||||
if user.ID > 0 {
|
||||
userKey = UserKey{
|
||||
UserID: user.ID,
|
||||
Key: key.Marshal(),
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
db.Create(&userKey)
|
||||
|
||||
// token is only usable once
|
||||
user.InviteToken = ""
|
||||
db.Update(&user)
|
||||
|
||||
ctx.SetValue(messageContextKey, fmt.Sprintf("Welcome %s!\n\nYour key is now associated with the user %q.\n", user.Name, user.Email))
|
||||
ctx.SetValue(userContextKey, user)
|
||||
} else {
|
||||
ctx.SetValue(userContextKey, User{Name: "Anonymous"})
|
||||
ctx.SetValue(errorContextKey, errors.New("your token is invalid or expired"))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// fallback
|
||||
ctx.SetValue(errorContextKey, errors.New("unknown ssh key"))
|
||||
ctx.SetValue(userContextKey, User{Name: "Anonymous"})
|
||||
return true
|
||||
}))
|
||||
|
||||
opts = append(opts, func(srv *ssh.Server) error {
|
||||
key, err := FindKeyByIdOrName(db, "host")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signer, err := gossh.ParsePrivateKey([]byte(key.PrivKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.AddHostKey(signer)
|
||||
return nil
|
||||
})
|
||||
|
||||
log.Printf("SSH Server accepting connections on %s", c.String("bind-address"))
|
||||
return ssh.ListenAndServe(c.String("bind-address"), nil, opts...)
|
||||
}
|
||||
|
||||
44
pkg/bastion/acl.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
type byWeight []*dbmodels.ACL
|
||||
|
||||
func (a byWeight) Len() int { return len(a) }
|
||||
func (a byWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byWeight) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
|
||||
|
||||
func checkACLs(user dbmodels.User, host dbmodels.Host) (string, error) {
|
||||
// shared ACLs between user and host
|
||||
aclMap := map[uint]*dbmodels.ACL{}
|
||||
for _, userGroup := range user.Groups {
|
||||
for _, userGroupACL := range userGroup.ACLs {
|
||||
for _, hostGroup := range host.Groups {
|
||||
for _, hostGroupACL := range hostGroup.ACLs {
|
||||
if userGroupACL.ID == hostGroupACL.ID {
|
||||
aclMap[userGroupACL.ID] = userGroupACL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// FIXME: add ACLs that match host pattern
|
||||
|
||||
// deny by default if no shared ACL
|
||||
if len(aclMap) == 0 {
|
||||
return string(dbmodels.ACLActionDeny), nil // default action
|
||||
}
|
||||
|
||||
// transform map to slice and sort it
|
||||
acls := make([]*dbmodels.ACL, 0, len(aclMap))
|
||||
for _, acl := range aclMap {
|
||||
acls = append(acls, acl)
|
||||
}
|
||||
sort.Sort(byWeight(acls))
|
||||
|
||||
return acls[0].Action, nil
|
||||
}
|
||||
50
pkg/bastion/acl_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
func TestCheckACLs(t *testing.T) {
|
||||
Convey("Testing CheckACLs", t, func(c C) {
|
||||
// create tmp dir
|
||||
tempDir, err := ioutil.TempDir("", "sshportal")
|
||||
c.So(err, ShouldBeNil)
|
||||
defer func() {
|
||||
c.So(os.RemoveAll(tempDir), ShouldBeNil)
|
||||
}()
|
||||
|
||||
// create sqlite db
|
||||
db, err := gorm.Open("sqlite3", filepath.Join(tempDir, "sshportal.db"))
|
||||
c.So(err, ShouldBeNil)
|
||||
db.LogMode(false)
|
||||
c.So(DBInit(db), ShouldBeNil)
|
||||
|
||||
// create dummy objects
|
||||
var hostGroup dbmodels.HostGroup
|
||||
err = dbmodels.HostGroupsByIdentifiers(db, []string{"default"}).First(&hostGroup).Error
|
||||
c.So(err, ShouldBeNil)
|
||||
db.Create(&dbmodels.Host{Groups: []*dbmodels.HostGroup{&hostGroup}})
|
||||
|
||||
//. load db
|
||||
var (
|
||||
hosts []dbmodels.Host
|
||||
users []dbmodels.User
|
||||
)
|
||||
db.Preload("Groups").Preload("Groups.ACLs").Find(&hosts)
|
||||
db.Preload("Groups").Preload("Groups.ACLs").Find(&users)
|
||||
|
||||
// test
|
||||
action, err := checkACLs(users[0], hosts[0])
|
||||
c.So(err, ShouldBeNil)
|
||||
c.So(action, ShouldEqual, dbmodels.ACLActionAllow)
|
||||
})
|
||||
}
|
||||
658
pkg/bastion/dbinit.go
Normal file
@@ -0,0 +1,658 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
gormigrate "gopkg.in/gormigrate.v1"
|
||||
"moul.io/sshportal/pkg/crypto"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
func DBInit(db *gorm.DB) error {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
db.Callback().Delete().Replace("gorm:delete", hardDeleteCallback)
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
|
||||
{
|
||||
ID: "1",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Setting struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
return tx.AutoMigrate(&Setting{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("settings").Error
|
||||
},
|
||||
}, {
|
||||
ID: "2",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type SSHKey struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string
|
||||
Type string
|
||||
Length uint
|
||||
Fingerprint string
|
||||
PrivKey string `sql:"size:10000"`
|
||||
PubKey string `sql:"size:10000"`
|
||||
Hosts []*dbmodels.Host `gorm:"ForeignKey:SSHKeyID"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&SSHKey{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("ssh_keys").Error
|
||||
},
|
||||
}, {
|
||||
ID: "3",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32"`
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Fingerprint string
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("hosts").Error
|
||||
},
|
||||
}, {
|
||||
ID: "4",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type UserKey struct {
|
||||
gorm.Model
|
||||
Key []byte `sql:"size:10000"`
|
||||
UserID uint ``
|
||||
User *dbmodels.User `gorm:"ForeignKey:UserID"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&UserKey{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("user_keys").Error
|
||||
},
|
||||
}, {
|
||||
ID: "5",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type User struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
IsAdmin bool
|
||||
Email string
|
||||
Name string
|
||||
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string
|
||||
InviteToken string
|
||||
}
|
||||
return tx.AutoMigrate(&User{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("users").Error
|
||||
},
|
||||
}, {
|
||||
ID: "6",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type UserGroup struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Users []*dbmodels.User `gorm:"many2many:user_user_groups;"`
|
||||
ACLs []*dbmodels.ACL `gorm:"many2many:user_group_acls;"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&UserGroup{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("user_groups").Error
|
||||
},
|
||||
}, {
|
||||
ID: "7",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type HostGroup struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Hosts []*dbmodels.Host `gorm:"many2many:host_host_groups;"`
|
||||
ACLs []*dbmodels.ACL `gorm:"many2many:host_group_acls;"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&HostGroup{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("host_groups").Error
|
||||
},
|
||||
}, {
|
||||
ID: "8",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type ACL struct {
|
||||
gorm.Model
|
||||
HostGroups []*dbmodels.HostGroup `gorm:"many2many:host_group_acls;"`
|
||||
UserGroups []*dbmodels.UserGroup `gorm:"many2many:user_group_acls;"`
|
||||
HostPattern string
|
||||
Action string
|
||||
Weight uint
|
||||
Comment string
|
||||
}
|
||||
|
||||
return tx.AutoMigrate(&ACL{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("acls").Error
|
||||
},
|
||||
}, {
|
||||
ID: "9",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&dbmodels.Setting{}).RemoveIndex("uix_settings_name")
|
||||
return db.Model(&dbmodels.Setting{}).AddUniqueIndex("uix_settings_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&dbmodels.Setting{}).RemoveIndex("uix_settings_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "10",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&dbmodels.SSHKey{}).RemoveIndex("uix_keys_name")
|
||||
return db.Model(&dbmodels.SSHKey{}).AddUniqueIndex("uix_keys_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&dbmodels.SSHKey{}).RemoveIndex("uix_keys_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "11",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&dbmodels.Host{}).RemoveIndex("uix_hosts_name")
|
||||
return db.Model(&dbmodels.Host{}).AddUniqueIndex("uix_hosts_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&dbmodels.Host{}).RemoveIndex("uix_hosts_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "12",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&dbmodels.User{}).RemoveIndex("uix_users_name")
|
||||
return db.Model(&dbmodels.User{}).AddUniqueIndex("uix_users_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&dbmodels.User{}).RemoveIndex("uix_users_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "13",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&dbmodels.UserGroup{}).RemoveIndex("uix_usergroups_name")
|
||||
return db.Model(&dbmodels.UserGroup{}).AddUniqueIndex("uix_usergroups_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&dbmodels.UserGroup{}).RemoveIndex("uix_usergroups_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "14",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&dbmodels.HostGroup{}).RemoveIndex("uix_hostgroups_name")
|
||||
return db.Model(&dbmodels.HostGroup{}).AddUniqueIndex("uix_hostgroups_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&dbmodels.HostGroup{}).RemoveIndex("uix_hostgroups_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "15",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type UserRole struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Users []*dbmodels.User `gorm:"many2many:user_user_roles"`
|
||||
}
|
||||
return tx.AutoMigrate(&UserRole{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("user_roles").Error
|
||||
},
|
||||
}, {
|
||||
ID: "16",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type User struct {
|
||||
gorm.Model
|
||||
IsAdmin bool
|
||||
Roles []*dbmodels.UserRole `gorm:"many2many:user_user_roles"`
|
||||
Email string `valid:"required,email"`
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string `valid:"optional"`
|
||||
InviteToken string `valid:"optional,length(10|60)"`
|
||||
}
|
||||
return tx.AutoMigrate(&User{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "17",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Create(&dbmodels.UserRole{Name: "admin"}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Where("name = ?", "admin").Delete(&dbmodels.UserRole{}).Error
|
||||
},
|
||||
}, {
|
||||
ID: "18",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var adminRole dbmodels.UserRole
|
||||
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var users []dbmodels.User
|
||||
if err := db.Preload("Roles").Where("is_admin = ?", true).Find(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
user.Roles = append(user.Roles, &adminRole)
|
||||
if err := tx.Save(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "19",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Roles []*dbmodels.UserRole `gorm:"many2many:user_user_roles"`
|
||||
Email string `valid:"required,email"`
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string `valid:"optional"`
|
||||
InviteToken string `valid:"optional,length(10|60)"`
|
||||
}
|
||||
return tx.AutoMigrate(&User{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "20",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Create(&dbmodels.UserRole{Name: "listhosts"}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Where("name = ?", "listhosts").Delete(&dbmodels.UserRole{}).Error
|
||||
},
|
||||
}, {
|
||||
ID: "21",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Session struct {
|
||||
gorm.Model
|
||||
StoppedAt time.Time `valid:"optional"`
|
||||
Status string `valid:"required"`
|
||||
User *dbmodels.User `gorm:"ForeignKey:UserID"`
|
||||
Host *dbmodels.Host `gorm:"ForeignKey:HostID"`
|
||||
UserID uint `valid:"optional"`
|
||||
HostID uint `valid:"optional"`
|
||||
ErrMsg string `valid:"optional"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
return tx.AutoMigrate(&Session{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("sessions").Error
|
||||
},
|
||||
}, {
|
||||
ID: "22",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Event struct {
|
||||
gorm.Model
|
||||
Author *dbmodels.User `gorm:"ForeignKey:AuthorID"`
|
||||
AuthorID uint `valid:"optional"`
|
||||
Domain string `valid:"required"`
|
||||
Action string `valid:"required"`
|
||||
Entity string `valid:"optional"`
|
||||
Args []byte `sql:"size:10000" valid:"optional,length(1|10000)"`
|
||||
}
|
||||
return tx.AutoMigrate(&Event{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("events").Error
|
||||
},
|
||||
}, {
|
||||
ID: "23",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type UserKey struct {
|
||||
gorm.Model
|
||||
Key []byte `sql:"size:10000" valid:"required,length(1|10000)"`
|
||||
AuthorizedKey string `sql:"size:10000" valid:"required,length(1|10000)"`
|
||||
UserID uint ``
|
||||
User *dbmodels.User `gorm:"ForeignKey:UserID"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
return tx.AutoMigrate(&UserKey{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "24",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var userKeys []dbmodels.UserKey
|
||||
if err := db.Find(&userKeys).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, userKey := range userKeys {
|
||||
key, err := gossh.ParsePublicKey(userKey.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userKey.AuthorizedKey = string(gossh.MarshalAuthorizedKey(key))
|
||||
if err := db.Model(&userKey).Updates(&userKey).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "25",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
|
||||
Addr string `valid:"required"`
|
||||
User string `valid:"optional"`
|
||||
Password string `valid:"optional"`
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000" valid:"optional"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Fingerprint string `valid:"optional"` // FIXME: replace with hostKey ?
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "26",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Session struct {
|
||||
gorm.Model
|
||||
StoppedAt *time.Time `sql:"index" valid:"optional"`
|
||||
Status string `valid:"required"`
|
||||
User *dbmodels.User `gorm:"ForeignKey:UserID"`
|
||||
Host *dbmodels.Host `gorm:"ForeignKey:HostID"`
|
||||
UserID uint `valid:"optional"`
|
||||
HostID uint `valid:"optional"`
|
||||
ErrMsg string `valid:"optional"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
return tx.AutoMigrate(&Session{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "27",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var sessions []dbmodels.Session
|
||||
if err := db.Find(&sessions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, session := range sessions {
|
||||
if session.StoppedAt != nil && session.StoppedAt.IsZero() {
|
||||
if err := db.Model(&session).Updates(map[string]interface{}{"stopped_at": nil}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "28",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32"`
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
URL string
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "29",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32"`
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
URL string
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string
|
||||
Hop *dbmodels.Host
|
||||
HopID uint
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
},
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
return err
|
||||
}
|
||||
dbmodels.NewEvent("system", "migrated").Log(db)
|
||||
|
||||
// create default ssh key
|
||||
var count uint
|
||||
if err := db.Table("ssh_keys").Where("name = ?", "default").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
key, err := crypto.NewSSHKey("rsa", 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.Name = "default"
|
||||
key.Comment = "created by sshportal"
|
||||
if err := db.Create(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create default host group
|
||||
if err := db.Table("host_groups").Where("name = ?", "default").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
hostGroup := dbmodels.HostGroup{
|
||||
Name: "default",
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
if err := db.Create(&hostGroup).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create default user group
|
||||
if err := db.Table("user_groups").Where("name = ?", "default").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
userGroup := dbmodels.UserGroup{
|
||||
Name: "default",
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
if err := db.Create(&userGroup).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create default acl
|
||||
if err := db.Table("acls").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
var defaultUserGroup dbmodels.UserGroup
|
||||
db.Where("name = ?", "default").First(&defaultUserGroup)
|
||||
var defaultHostGroup dbmodels.HostGroup
|
||||
db.Where("name = ?", "default").First(&defaultHostGroup)
|
||||
acl := dbmodels.ACL{
|
||||
UserGroups: []*dbmodels.UserGroup{&defaultUserGroup},
|
||||
HostGroups: []*dbmodels.HostGroup{&defaultHostGroup},
|
||||
Action: "allow",
|
||||
//HostPattern: "",
|
||||
//Weight: 0,
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
if err := db.Create(&acl).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create admin user
|
||||
var defaultUserGroup dbmodels.UserGroup
|
||||
db.Where("name = ?", "default").First(&defaultUserGroup)
|
||||
if err := db.Table("users").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
// if no admin, create an account for the first connection
|
||||
inviteToken := randStringBytes(16)
|
||||
if os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN") != "" {
|
||||
inviteToken = os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN")
|
||||
}
|
||||
var adminRole dbmodels.UserRole
|
||||
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var username string
|
||||
if currentUser, err := user.Current(); err == nil {
|
||||
username = currentUser.Username
|
||||
}
|
||||
if username == "" {
|
||||
username = os.Getenv("USER")
|
||||
}
|
||||
username = strings.ToLower(username)
|
||||
if username == "" {
|
||||
username = "admin" // fallback username
|
||||
}
|
||||
user := dbmodels.User{
|
||||
Name: username,
|
||||
Email: fmt.Sprintf("%s@localhost", username),
|
||||
Comment: "created by sshportal",
|
||||
Roles: []*dbmodels.UserRole{&adminRole},
|
||||
InviteToken: inviteToken,
|
||||
Groups: []*dbmodels.UserGroup{&defaultUserGroup},
|
||||
}
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("info 'admin' user created, use the user 'invite:%s' to associate a public key with this account", user.InviteToken)
|
||||
}
|
||||
|
||||
// create host ssh key
|
||||
if err := db.Table("ssh_keys").Where("name = ?", "host").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
key, err := crypto.NewSSHKey("rsa", 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.Name = "host"
|
||||
key.Comment = "created by sshportal"
|
||||
if err := db.Create(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// close unclosed connections
|
||||
return db.Table("sessions").Where("status = ?", "active").Updates(&dbmodels.Session{
|
||||
Status: string(dbmodels.SessionStatusClosed),
|
||||
ErrMsg: "sshportal was halted while the connection was still active",
|
||||
}).Error
|
||||
}
|
||||
|
||||
func hardDeleteCallback(scope *gorm.Scope) {
|
||||
if !scope.HasError() {
|
||||
var extraOption string
|
||||
if str, ok := scope.Get("gorm:delete_option"); ok {
|
||||
extraOption = fmt.Sprint(str)
|
||||
}
|
||||
|
||||
/* #nosec */
|
||||
scope.Raw(fmt.Sprintf(
|
||||
"DELETE FROM %v%v%v",
|
||||
scope.QuotedTableName(),
|
||||
addExtraSpaceIfExist(scope.CombinedConditionSql()),
|
||||
addExtraSpaceIfExist(extraOption),
|
||||
)).Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func addExtraSpaceIfExist(str string) string {
|
||||
if str != "" {
|
||||
return " " + str
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func randStringBytes(n int) string {
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
71
pkg/bastion/logtunnel.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type logTunnel struct {
|
||||
host string
|
||||
channel ssh.Channel
|
||||
writer io.WriteCloser
|
||||
}
|
||||
|
||||
type logTunnelForwardData struct {
|
||||
DestinationHost string
|
||||
DestinationPort uint32
|
||||
SourceHost string
|
||||
SourcePort uint32
|
||||
}
|
||||
|
||||
func writeHeader(fd io.Writer, length int) {
|
||||
t := time.Now()
|
||||
|
||||
tv := syscall.NsecToTimeval(t.UnixNano())
|
||||
|
||||
if err := binary.Write(fd, binary.LittleEndian, int32(tv.Sec)); err != nil {
|
||||
log.Printf("failed to write log header: %v", err)
|
||||
}
|
||||
if err := binary.Write(fd, binary.LittleEndian, tv.Usec); err != nil {
|
||||
log.Printf("failed to write log header: %v", err)
|
||||
}
|
||||
if err := binary.Write(fd, binary.LittleEndian, int32(length)); err != nil {
|
||||
log.Printf("failed to write log header: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newLogTunnel(channel ssh.Channel, writer io.WriteCloser, host string) io.ReadWriteCloser {
|
||||
return &logTunnel{
|
||||
host: host,
|
||||
channel: channel,
|
||||
writer: writer,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logTunnel) Read(data []byte) (int, error) {
|
||||
return 0, errors.New("logTunnel.Read is not implemented")
|
||||
}
|
||||
|
||||
func (l *logTunnel) Write(data []byte) (int, error) {
|
||||
writeHeader(l.writer, len(data)+len(l.host+": "))
|
||||
if _, err := l.writer.Write([]byte(l.host + ": ")); err != nil {
|
||||
log.Printf("failed to write log: %v", err)
|
||||
}
|
||||
if _, err := l.writer.Write(data); err != nil {
|
||||
log.Printf("failed to write log: %v", err)
|
||||
}
|
||||
|
||||
return l.channel.Write(data)
|
||||
}
|
||||
|
||||
func (l *logTunnel) Close() error {
|
||||
l.writer.Close()
|
||||
|
||||
return l.channel.Close()
|
||||
}
|
||||
206
pkg/bastion/session.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/sabban/bastion/pkg/logchannel"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type sessionConfig struct {
|
||||
Addr string
|
||||
Logs string
|
||||
ClientConfig *gossh.ClientConfig
|
||||
}
|
||||
|
||||
func multiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []sessionConfig, sessionID uint) error {
|
||||
var lastClient *gossh.Client
|
||||
switch newChan.ChannelType() {
|
||||
case "session":
|
||||
lch, lreqs, err := newChan.Accept()
|
||||
// TODO: defer clean closer
|
||||
if err != nil {
|
||||
// TODO: trigger event callback
|
||||
return nil
|
||||
}
|
||||
|
||||
// go through all the hops
|
||||
for _, config := range configs {
|
||||
var client *gossh.Client
|
||||
if lastClient == nil {
|
||||
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
|
||||
} else {
|
||||
rconn, err := lastClient.Dial("tcp", config.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ncc, chans, reqs, err := gossh.NewClientConn(rconn, config.Addr, config.ClientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client = gossh.NewClient(ncc, chans, reqs)
|
||||
}
|
||||
if err != nil {
|
||||
lch.Close() // fix #56
|
||||
return err
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
lastClient = client
|
||||
}
|
||||
|
||||
rch, rreqs, err := lastClient.OpenChannel("session", []byte{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := conn.User()
|
||||
actx := ctx.Value(authContextKey).(*authContext)
|
||||
username := actx.user.Name
|
||||
// pipe everything
|
||||
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, username, sessionID, newChan)
|
||||
case "direct-tcpip":
|
||||
lch, lreqs, err := newChan.Accept()
|
||||
// TODO: defer clean closer
|
||||
if err != nil {
|
||||
// TODO: trigger event callback
|
||||
return nil
|
||||
}
|
||||
|
||||
// go through all the hops
|
||||
for _, config := range configs {
|
||||
var client *gossh.Client
|
||||
if lastClient == nil {
|
||||
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
|
||||
} else {
|
||||
rconn, err := lastClient.Dial("tcp", config.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ncc, chans, reqs, err := gossh.NewClientConn(rconn, config.Addr, config.ClientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client = gossh.NewClient(ncc, chans, reqs)
|
||||
}
|
||||
if err != nil {
|
||||
lch.Close()
|
||||
return err
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
lastClient = client
|
||||
}
|
||||
|
||||
d := logTunnelForwardData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
return err
|
||||
}
|
||||
rch, rreqs, err := lastClient.OpenChannel("direct-tcpip", newChan.ExtraData())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := conn.User()
|
||||
actx := ctx.Value(authContextKey).(*authContext)
|
||||
username := actx.user.Name
|
||||
// pipe everything
|
||||
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, username, sessionID, newChan)
|
||||
default:
|
||||
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
|
||||
log.Printf("failed to reject chan: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocation string, user string, username string, sessionID uint, newChan gossh.NewChannel) error {
|
||||
defer func() {
|
||||
_ = lch.Close()
|
||||
_ = rch.Close()
|
||||
}()
|
||||
|
||||
errch := make(chan error, 1)
|
||||
channeltype := newChan.ChannelType()
|
||||
|
||||
filename := strings.Join([]string{logsLocation, "/", user, "-", username, "-", channeltype, "-", fmt.Sprint(sessionID), "-", time.Now().Format(time.RFC3339)}, "") // get user
|
||||
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0440)
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("error: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Session %v is recorded in %v", channeltype, filename)
|
||||
if channeltype == "session" {
|
||||
wrappedlch := logchannel.New(lch, f)
|
||||
go func() {
|
||||
_, _ = io.Copy(wrappedlch, rch)
|
||||
errch <- errors.New("lch closed the connection")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(rch, lch)
|
||||
errch <- errors.New("rch closed the connection")
|
||||
}()
|
||||
}
|
||||
if channeltype == "direct-tcpip" {
|
||||
d := logTunnelForwardData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
return err
|
||||
}
|
||||
wrappedlch := newLogTunnel(lch, f, d.SourceHost)
|
||||
wrappedrch := newLogTunnel(rch, f, d.DestinationHost)
|
||||
go func() {
|
||||
_, _ = io.Copy(wrappedlch, rch)
|
||||
errch <- errors.New("lch closed the connection")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(wrappedrch, lch)
|
||||
errch <- errors.New("rch closed the connection")
|
||||
}()
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case req := <-lreqs: // forward ssh requests from local to remote
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := rch.SendRequest(req.Type, req.WantReply, req.Payload)
|
||||
if req.Type == "exec" {
|
||||
wrappedlch := logchannel.New(lch, f)
|
||||
command := append(req.Payload, []byte("\n")...)
|
||||
if _, err := wrappedlch.LogWrite(command); err != nil {
|
||||
log.Printf("failed to write log: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err2 := req.Reply(b, nil); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
case req := <-rreqs: // forward ssh requests from remote to local
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := lch.SendRequest(req.Type, req.WantReply, req.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err2 := req.Reply(b, nil); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
case err := <-errch:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
2249
pkg/bastion/shell.go
Normal file
350
pkg/bastion/ssh.go
Normal file
@@ -0,0 +1,350 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"moul.io/sshportal/pkg/crypto"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
type sshportalContextKey string
|
||||
|
||||
var authContextKey = sshportalContextKey("auth")
|
||||
|
||||
type authContext struct {
|
||||
message string
|
||||
err error
|
||||
user dbmodels.User
|
||||
inputUsername string
|
||||
db *gorm.DB
|
||||
userKey dbmodels.UserKey
|
||||
logsLocation string
|
||||
aesKey string
|
||||
dbDriver, dbURL string
|
||||
bindAddr string
|
||||
demo, debug bool
|
||||
authMethod string
|
||||
authSuccess bool
|
||||
}
|
||||
|
||||
type userType string
|
||||
|
||||
const (
|
||||
userTypeHealthcheck userType = "healthcheck"
|
||||
userTypeBastion userType = "bastion"
|
||||
userTypeInvite userType = "invite"
|
||||
userTypeShell userType = "shell"
|
||||
)
|
||||
|
||||
func (c authContext) userType() userType {
|
||||
switch {
|
||||
case c.inputUsername == "healthcheck":
|
||||
return userTypeHealthcheck
|
||||
case c.inputUsername == c.user.Name || c.inputUsername == c.user.Email || c.inputUsername == "admin":
|
||||
return userTypeShell
|
||||
case strings.HasPrefix(c.inputUsername, "invite:"):
|
||||
return userTypeInvite
|
||||
default:
|
||||
return userTypeBastion
|
||||
}
|
||||
}
|
||||
|
||||
func dynamicHostKey(db *gorm.DB, host *dbmodels.Host) gossh.HostKeyCallback {
|
||||
return func(hostname string, remote net.Addr, key gossh.PublicKey) error {
|
||||
if len(host.HostKey) == 0 {
|
||||
log.Println("Discovering host fingerprint...")
|
||||
return db.Model(host).Update("HostKey", key.Marshal()).Error
|
||||
}
|
||||
|
||||
if !bytes.Equal(host.HostKey, key.Marshal()) {
|
||||
return fmt.Errorf("ssh: host key mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var DefaultChannelHandler ssh.ChannelHandler = func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {}
|
||||
|
||||
func ChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
|
||||
switch newChan.ChannelType() {
|
||||
case "session":
|
||||
case "direct-tcpip":
|
||||
default:
|
||||
// TODO: handle direct-tcp (only for ssh scheme)
|
||||
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
|
||||
log.Printf("error: failed to reject channel: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
actx := ctx.Value(authContextKey).(*authContext)
|
||||
|
||||
switch actx.userType() {
|
||||
case userTypeBastion:
|
||||
log.Printf("New connection(bastion): sshUser=%q remote=%q local=%q dbUser=id:%d,email:%s", conn.User(), conn.RemoteAddr(), conn.LocalAddr(), actx.user.ID, actx.user.Email)
|
||||
host, err := dbmodels.HostByName(actx.db, actx.inputUsername)
|
||||
if err != nil {
|
||||
ch, _, err2 := newChan.Accept()
|
||||
if err2 != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(ch, "error: %v\n", err)
|
||||
// FIXME: force close all channels
|
||||
_ = ch.Close()
|
||||
return
|
||||
}
|
||||
|
||||
switch host.Scheme() {
|
||||
case dbmodels.BastionSchemeSSH:
|
||||
sessionConfigs := make([]sessionConfig, 0)
|
||||
currentHost := host
|
||||
for currentHost != nil {
|
||||
clientConfig, err2 := bastionClientConfig(ctx, currentHost)
|
||||
if err2 != nil {
|
||||
ch, _, err3 := newChan.Accept()
|
||||
if err3 != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(ch, "error: %v\n", err2)
|
||||
// FIXME: force close all channels
|
||||
_ = ch.Close()
|
||||
return
|
||||
}
|
||||
sessionConfigs = append([]sessionConfig{{
|
||||
Addr: currentHost.DialAddr(),
|
||||
ClientConfig: clientConfig,
|
||||
Logs: actx.logsLocation,
|
||||
}}, sessionConfigs...)
|
||||
if currentHost.HopID != 0 {
|
||||
var newHost dbmodels.Host
|
||||
actx.db.Model(currentHost).Related(&newHost, "HopID")
|
||||
hostname := newHost.Name
|
||||
currentHost, _ = dbmodels.HostByName(actx.db, hostname)
|
||||
} else {
|
||||
currentHost = nil
|
||||
}
|
||||
}
|
||||
|
||||
sess := dbmodels.Session{
|
||||
UserID: actx.user.ID,
|
||||
HostID: host.ID,
|
||||
Status: string(dbmodels.SessionStatusActive),
|
||||
}
|
||||
if err = actx.db.Create(&sess).Error; err != nil {
|
||||
ch, _, err2 := newChan.Accept()
|
||||
if err2 != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(ch, "error: %v\n", err)
|
||||
_ = ch.Close()
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
err = multiChannelHandler(srv, conn, newChan, ctx, sessionConfigs, sess.ID)
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
sessUpdate := dbmodels.Session{
|
||||
Status: string(dbmodels.SessionStatusClosed),
|
||||
ErrMsg: fmt.Sprintf("%v", err),
|
||||
StoppedAt: &now,
|
||||
}
|
||||
switch sessUpdate.ErrMsg {
|
||||
case "lch closed the connection", "rch closed the connection":
|
||||
sessUpdate.ErrMsg = ""
|
||||
}
|
||||
actx.db.Model(&sess).Updates(&sessUpdate)
|
||||
}()
|
||||
case dbmodels.BastionSchemeTelnet:
|
||||
tmpSrv := ssh.Server{
|
||||
// PtyCallback: srv.PtyCallback,
|
||||
Handler: telnetHandler(host),
|
||||
}
|
||||
DefaultChannelHandler(&tmpSrv, conn, newChan, ctx)
|
||||
default:
|
||||
ch, _, err2 := newChan.Accept()
|
||||
if err2 != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(ch, "error: unknown bastion scheme: %q\n", host.Scheme())
|
||||
// FIXME: force close all channels
|
||||
_ = ch.Close()
|
||||
}
|
||||
default: // shell
|
||||
DefaultChannelHandler(srv, conn, newChan, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func bastionClientConfig(ctx ssh.Context, host *dbmodels.Host) (*gossh.ClientConfig, error) {
|
||||
actx := ctx.Value(authContextKey).(*authContext)
|
||||
|
||||
crypto.HostDecrypt(actx.aesKey, host)
|
||||
crypto.SSHKeyDecrypt(actx.aesKey, host.SSHKey)
|
||||
|
||||
clientConfig, err := host.ClientConfig(dynamicHostKey(actx.db, host))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tmpUser dbmodels.User
|
||||
if err = actx.db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", actx.user.ID).First(&tmpUser).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tmpHost dbmodels.Host
|
||||
if err = actx.db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", host.ID).First(&tmpHost).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
action, err2 := checkACLs(tmpUser, tmpHost)
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
|
||||
switch action {
|
||||
case string(dbmodels.ACLActionAllow):
|
||||
case string(dbmodels.ACLActionDeny):
|
||||
return nil, fmt.Errorf("you don't have permission to that host")
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid ACL action: %q", action)
|
||||
}
|
||||
return clientConfig, nil
|
||||
}
|
||||
|
||||
func ShellHandler(s ssh.Session, version, gitSha, gitTag, gitBranch string) {
|
||||
actx := s.Context().Value(authContextKey).(*authContext)
|
||||
if actx.userType() != userTypeHealthcheck {
|
||||
log.Printf("New connection(shell): sshUser=%q remote=%q local=%q command=%q dbUser=id:%d,email:%s", s.User(), s.RemoteAddr(), s.LocalAddr(), s.Command(), actx.user.ID, actx.user.Email)
|
||||
}
|
||||
|
||||
if actx.err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", actx.err)
|
||||
_ = s.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
if actx.message != "" {
|
||||
fmt.Fprint(s, actx.message)
|
||||
}
|
||||
|
||||
switch actx.userType() {
|
||||
case userTypeHealthcheck:
|
||||
fmt.Fprintln(s, "OK")
|
||||
return
|
||||
case userTypeShell:
|
||||
if err := shell(s, version, gitSha, gitTag, gitBranch); err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
_ = s.Exit(1)
|
||||
}
|
||||
return
|
||||
case userTypeInvite:
|
||||
// do nothing (message was printed at the beginning of the function)
|
||||
return
|
||||
}
|
||||
panic("should not happen")
|
||||
}
|
||||
|
||||
func PasswordAuthHandler(db *gorm.DB, logsLocation, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PasswordHandler {
|
||||
return func(ctx ssh.Context, pass string) bool {
|
||||
actx := &authContext{
|
||||
db: db,
|
||||
inputUsername: ctx.User(),
|
||||
logsLocation: logsLocation,
|
||||
aesKey: aesKey,
|
||||
dbDriver: dbDriver,
|
||||
dbURL: dbURL,
|
||||
bindAddr: bindAddr,
|
||||
demo: demo,
|
||||
authMethod: "password",
|
||||
}
|
||||
actx.authSuccess = actx.userType() == userTypeHealthcheck
|
||||
ctx.SetValue(authContextKey, actx)
|
||||
return actx.authSuccess
|
||||
}
|
||||
}
|
||||
|
||||
func PrivateKeyFromDB(db *gorm.DB, aesKey string) func(*ssh.Server) error {
|
||||
return func(srv *ssh.Server) error {
|
||||
var key dbmodels.SSHKey
|
||||
if err := dbmodels.SSHKeysByIdentifiers(db, []string{"host"}).First(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
crypto.SSHKeyDecrypt(aesKey, &key)
|
||||
|
||||
signer, err := gossh.ParsePrivateKey([]byte(key.PrivKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.AddHostKey(signer)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func PublicKeyAuthHandler(db *gorm.DB, logsLocation, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PublicKeyHandler {
|
||||
return func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
actx := &authContext{
|
||||
db: db,
|
||||
inputUsername: ctx.User(),
|
||||
logsLocation: logsLocation,
|
||||
aesKey: aesKey,
|
||||
dbDriver: dbDriver,
|
||||
dbURL: dbURL,
|
||||
bindAddr: bindAddr,
|
||||
demo: demo,
|
||||
authMethod: "pubkey",
|
||||
authSuccess: true,
|
||||
}
|
||||
ctx.SetValue(authContextKey, actx)
|
||||
|
||||
// lookup user by key
|
||||
db.Where("authorized_key = ?", string(gossh.MarshalAuthorizedKey(key))).First(&actx.userKey)
|
||||
if actx.userKey.UserID > 0 {
|
||||
db.Preload("Roles").Where("id = ?", actx.userKey.UserID).First(&actx.user)
|
||||
if actx.userType() == userTypeInvite {
|
||||
actx.err = fmt.Errorf("invites are only supported for new SSH keys; your ssh key is already associated with the user %q", actx.user.Email)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// handle invite "links"
|
||||
if actx.userType() == userTypeInvite {
|
||||
inputToken := strings.Split(actx.inputUsername, ":")[1]
|
||||
if len(inputToken) > 0 {
|
||||
db.Where("invite_token = ?", inputToken).First(&actx.user)
|
||||
}
|
||||
if actx.user.ID > 0 {
|
||||
actx.userKey = dbmodels.UserKey{
|
||||
UserID: actx.user.ID,
|
||||
Key: key.Marshal(),
|
||||
Comment: "created by sshportal",
|
||||
AuthorizedKey: string(gossh.MarshalAuthorizedKey(key)),
|
||||
}
|
||||
db.Create(&actx.userKey)
|
||||
|
||||
// token is only usable once
|
||||
actx.user.InviteToken = ""
|
||||
db.Model(&actx.user).Updates(&actx.user)
|
||||
|
||||
actx.message = fmt.Sprintf("Welcome %s!\n\nYour key is now associated with the user %q.\n", actx.user.Name, actx.user.Email)
|
||||
} else {
|
||||
actx.user = dbmodels.User{Name: "Anonymous"}
|
||||
actx.err = errors.New("your token is invalid or expired")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// fallback
|
||||
actx.err = errors.New("unknown ssh key")
|
||||
actx.user = dbmodels.User{Name: "Anonymous"}
|
||||
return true
|
||||
}
|
||||
}
|
||||
88
pkg/bastion/telnet.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
oi "github.com/reiver/go-oi"
|
||||
telnet "github.com/reiver/go-telnet"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
type bastionTelnetCaller struct {
|
||||
ssh ssh.Session
|
||||
}
|
||||
|
||||
func (caller bastionTelnetCaller) CallTELNET(ctx telnet.Context, w telnet.Writer, r telnet.Reader) {
|
||||
go func(writer io.Writer, reader io.Reader) {
|
||||
var buffer [1]byte // Seems like the length of the buffer needs to be small, otherwise will have to wait for buffer to fill up.
|
||||
p := buffer[:]
|
||||
|
||||
for {
|
||||
// Read 1 byte.
|
||||
n, err := reader.Read(p)
|
||||
if n <= 0 && err == nil {
|
||||
continue
|
||||
} else if n <= 0 && err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if _, err = oi.LongWrite(writer, p); err != nil {
|
||||
log.Printf("telnet longwrite failed: %v", err)
|
||||
}
|
||||
}
|
||||
}(caller.ssh, r)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
var p []byte
|
||||
|
||||
var crlfBuffer = [2]byte{'\r', '\n'}
|
||||
crlf := crlfBuffer[:]
|
||||
|
||||
scanner := bufio.NewScanner(caller.ssh)
|
||||
scanner.Split(scannerSplitFunc)
|
||||
|
||||
for scanner.Scan() {
|
||||
buffer.Write(scanner.Bytes())
|
||||
buffer.Write(crlf)
|
||||
|
||||
p = buffer.Bytes()
|
||||
|
||||
n, err := oi.LongWrite(w, p)
|
||||
if nil != err {
|
||||
break
|
||||
}
|
||||
if expected, actual := int64(len(p)), n; expected != actual {
|
||||
err := fmt.Errorf("transmission problem: tried sending %d bytes, but actually only sent %d bytes", expected, actual)
|
||||
fmt.Fprint(caller.ssh, err.Error())
|
||||
return
|
||||
}
|
||||
buffer.Reset()
|
||||
}
|
||||
|
||||
// Wait a bit to receive data from the server (that we would send to io.Stdout).
|
||||
time.Sleep(3 * time.Millisecond)
|
||||
}
|
||||
|
||||
func scannerSplitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF {
|
||||
return 0, nil, nil
|
||||
}
|
||||
return bufio.ScanLines(data, atEOF)
|
||||
}
|
||||
|
||||
func telnetHandler(host *dbmodels.Host) ssh.Handler {
|
||||
return func(s ssh.Session) {
|
||||
// FIXME: log session in db
|
||||
//actx := s.Context().Value(authContextKey).(*authContext)
|
||||
caller := bastionTelnetCaller{ssh: s}
|
||||
if err := telnet.DialToAndCall(host.DialAddr(), caller); err != nil {
|
||||
fmt.Fprintf(s, "error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
166
pkg/crypto/crypto.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package crypto // import "moul.io/sshportal/pkg/crypto"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
func NewSSHKey(keyType string, length uint) (*dbmodels.SSHKey, error) {
|
||||
key := dbmodels.SSHKey{
|
||||
Type: keyType,
|
||||
Length: length,
|
||||
}
|
||||
|
||||
// generate the private key
|
||||
if keyType != "rsa" {
|
||||
return nil, fmt.Errorf("key type not supported: %q", key.Type)
|
||||
}
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// convert priv key to x509 format
|
||||
var pemKey = &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||
}
|
||||
buf := bytes.NewBufferString("")
|
||||
if err = pem.Encode(buf, pemKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key.PrivKey = buf.String()
|
||||
|
||||
// generte authorized-key formatted pubkey output
|
||||
pub, err := gossh.NewPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key.PubKey = strings.TrimSpace(string(gossh.MarshalAuthorizedKey(pub)))
|
||||
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func ImportSSHKey(keyValue string) (*dbmodels.SSHKey, error) {
|
||||
key := dbmodels.SSHKey{
|
||||
Type: "rsa",
|
||||
}
|
||||
|
||||
parsedKey, err := gossh.ParseRawPrivateKey([]byte(keyValue))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var privateKey *rsa.PrivateKey
|
||||
var ok bool
|
||||
if privateKey, ok = parsedKey.(*rsa.PrivateKey); !ok {
|
||||
return nil, errors.New("key type not supported")
|
||||
}
|
||||
key.Length = uint(privateKey.PublicKey.N.BitLen())
|
||||
// convert priv key to x509 format
|
||||
var pemKey = &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||
}
|
||||
buf := bytes.NewBufferString("")
|
||||
if err = pem.Encode(buf, pemKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key.PrivKey = buf.String()
|
||||
|
||||
// generte authorized-key formatted pubkey output
|
||||
pub, err := gossh.NewPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key.PubKey = strings.TrimSpace(string(gossh.MarshalAuthorizedKey(pub)))
|
||||
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func encrypt(key []byte, text string) (string, error) {
|
||||
plaintext := []byte(text)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
|
||||
return base64.URLEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
func decrypt(key []byte, cryptoText string) (string, error) {
|
||||
ciphertext, _ := base64.URLEncoding.DecodeString(cryptoText)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return "", fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext, ciphertext)
|
||||
return fmt.Sprintf("%s", ciphertext), nil
|
||||
}
|
||||
|
||||
func safeDecrypt(key []byte, cryptoText string) string {
|
||||
if len(key) == 0 {
|
||||
return cryptoText
|
||||
}
|
||||
out, err := decrypt(key, cryptoText)
|
||||
if err != nil {
|
||||
return cryptoText
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func HostEncrypt(aesKey string, host *dbmodels.Host) (err error) {
|
||||
if aesKey == "" {
|
||||
return nil
|
||||
}
|
||||
if host.Password != "" {
|
||||
host.Password, err = encrypt([]byte(aesKey), host.Password)
|
||||
}
|
||||
return
|
||||
}
|
||||
func HostDecrypt(aesKey string, host *dbmodels.Host) {
|
||||
if aesKey == "" {
|
||||
return
|
||||
}
|
||||
if host.Password != "" {
|
||||
host.Password = safeDecrypt([]byte(aesKey), host.Password)
|
||||
}
|
||||
}
|
||||
|
||||
func SSHKeyEncrypt(aesKey string, key *dbmodels.SSHKey) (err error) {
|
||||
if aesKey == "" {
|
||||
return nil
|
||||
}
|
||||
key.PrivKey, err = encrypt([]byte(aesKey), key.PrivKey)
|
||||
return
|
||||
}
|
||||
func SSHKeyDecrypt(aesKey string, key *dbmodels.SSHKey) {
|
||||
if aesKey == "" {
|
||||
return
|
||||
}
|
||||
key.PrivKey = safeDecrypt([]byte(aesKey), key.PrivKey)
|
||||
}
|
||||
447
pkg/dbmodels/dbmodels.go
Normal file
@@ -0,0 +1,447 @@
|
||||
package dbmodels // import "moul.io/sshportal/pkg/dbmodels"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/jinzhu/gorm"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
SSHKeys []*SSHKey `json:"keys"`
|
||||
Hosts []*Host `json:"hosts"`
|
||||
UserKeys []*UserKey `json:"user_keys"`
|
||||
Users []*User `json:"users"`
|
||||
UserGroups []*UserGroup `json:"user_groups"`
|
||||
HostGroups []*HostGroup `json:"host_groups"`
|
||||
ACLs []*ACL `json:"acls"`
|
||||
Settings []*Setting `json:"settings"`
|
||||
Events []*Event `json:"events"`
|
||||
Sessions []*Session `json:"sessions"`
|
||||
// FIXME: add latest migration
|
||||
Date time.Time `json:"date"`
|
||||
}
|
||||
|
||||
type Setting struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required"`
|
||||
Value string `valid:"required"`
|
||||
}
|
||||
|
||||
// SSHKey defines a ssh client key (used by sshportal to connect to remote hosts)
|
||||
type SSHKey struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Type string `valid:"required"`
|
||||
Length uint `valid:"required"`
|
||||
Fingerprint string `valid:"optional"`
|
||||
PrivKey string `sql:"size:10000" valid:"required"`
|
||||
PubKey string `sql:"size:10000" valid:"optional"`
|
||||
Hosts []*Host `gorm:"ForeignKey:SSHKeyID"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type Host struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32" valid:"required,length(1|32)"`
|
||||
Addr string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
|
||||
User string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
|
||||
Password string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
|
||||
URL string `valid:"optional"`
|
||||
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000" valid:"optional"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string `valid:"optional"`
|
||||
Hop *Host
|
||||
HopID uint
|
||||
}
|
||||
|
||||
// UserKey defines a user public key used by sshportal to identify the user
|
||||
type UserKey struct {
|
||||
gorm.Model
|
||||
Key []byte `sql:"size:10000" valid:"length(1|10000)"`
|
||||
AuthorizedKey string `sql:"size:10000" valid:"required,length(1|10000)"`
|
||||
UserID uint ``
|
||||
User *User `gorm:"ForeignKey:UserID"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type UserRole struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Users []*User `gorm:"many2many:user_user_roles"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Roles []*UserRole `gorm:"many2many:user_user_roles"`
|
||||
Email string `valid:"required,email"`
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Keys []*UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string `valid:"optional"`
|
||||
InviteToken string `valid:"optional,length(10|60)"`
|
||||
}
|
||||
|
||||
type UserGroup struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Users []*User `gorm:"many2many:user_user_groups;"`
|
||||
ACLs []*ACL `gorm:"many2many:user_group_acls;"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type HostGroup struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Hosts []*Host `gorm:"many2many:host_host_groups;"`
|
||||
ACLs []*ACL `gorm:"many2many:host_group_acls;"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type ACL struct {
|
||||
gorm.Model
|
||||
HostGroups []*HostGroup `gorm:"many2many:host_group_acls;"`
|
||||
UserGroups []*UserGroup `gorm:"many2many:user_group_acls;"`
|
||||
HostPattern string `valid:"optional"`
|
||||
Action string `valid:"required"`
|
||||
Weight uint ``
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
gorm.Model
|
||||
StoppedAt *time.Time `sql:"index" valid:"optional"`
|
||||
Status string `valid:"required"`
|
||||
User *User `gorm:"ForeignKey:UserID"`
|
||||
Host *Host `gorm:"ForeignKey:HostID"`
|
||||
UserID uint `valid:"optional"`
|
||||
HostID uint `valid:"optional"`
|
||||
ErrMsg string `valid:"optional"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
gorm.Model
|
||||
Author *User `gorm:"ForeignKey:AuthorID"`
|
||||
AuthorID uint `valid:"optional"`
|
||||
Domain string `valid:"required"`
|
||||
Action string `valid:"required"`
|
||||
Entity string `valid:"optional"`
|
||||
Args []byte `sql:"size:10000" valid:"optional,length(1|10000)" json:"-"`
|
||||
ArgsMap map[string]interface{} `gorm:"-" json:"Args"`
|
||||
}
|
||||
|
||||
type SessionStatus string
|
||||
|
||||
const (
|
||||
SessionStatusUnknown SessionStatus = "unknown"
|
||||
SessionStatusActive SessionStatus = "active"
|
||||
SessionStatusClosed SessionStatus = "closed"
|
||||
)
|
||||
|
||||
type ACLAction string
|
||||
|
||||
const (
|
||||
ACLActionAllow ACLAction = "allow"
|
||||
ACLActionDeny ACLAction = "deny"
|
||||
)
|
||||
|
||||
type BastionScheme string
|
||||
|
||||
const (
|
||||
BastionSchemeSSH BastionScheme = "ssh"
|
||||
BastionSchemeTelnet BastionScheme = "telnet"
|
||||
)
|
||||
|
||||
func init() {
|
||||
unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*")
|
||||
|
||||
govalidator.CustomTypeTagMap.Set("unix_user", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||
name, ok := i.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return unixUserRegexp.MatchString(name)
|
||||
}))
|
||||
}
|
||||
|
||||
// Host helpers
|
||||
|
||||
func (host *Host) DialAddr() string {
|
||||
return fmt.Sprintf("%s:%d", host.Hostname(), host.Port())
|
||||
}
|
||||
func (host *Host) String() string {
|
||||
if host.URL != "" {
|
||||
return host.URL
|
||||
} else if host.Addr != "" { // to be removed in a future version in favor of URL
|
||||
if host.Password != "" {
|
||||
return fmt.Sprintf("ssh://%s:%s@%s", host.User, strings.Repeat("*", 4), host.Addr)
|
||||
}
|
||||
return fmt.Sprintf("ssh://%s@%s", host.User, host.Addr)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (host *Host) Scheme() BastionScheme {
|
||||
if host.URL != "" {
|
||||
u, err := url.Parse(host.URL)
|
||||
if err != nil {
|
||||
return BastionSchemeSSH
|
||||
}
|
||||
return BastionScheme(u.Scheme)
|
||||
} else if host.Addr != "" {
|
||||
return BastionSchemeSSH
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (host *Host) Hostname() string {
|
||||
if host.URL != "" {
|
||||
u, err := url.Parse(host.URL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return u.Hostname()
|
||||
} else if host.Addr != "" { // to be removed in a future version in favor of URL
|
||||
return strings.Split(host.Addr, ":")[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (host *Host) Username() string {
|
||||
if host.URL != "" {
|
||||
u, err := url.Parse(host.URL)
|
||||
if err != nil {
|
||||
return "root"
|
||||
}
|
||||
if u.User != nil {
|
||||
return u.User.Username()
|
||||
}
|
||||
} else if host.User != "" { // to be removed in a future version in favor of URL
|
||||
return host.User
|
||||
}
|
||||
return "root"
|
||||
}
|
||||
func (host *Host) Passwd() string {
|
||||
if host.URL != "" {
|
||||
u, err := url.Parse(host.URL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if u.User != nil {
|
||||
password, _ := u.User.Password()
|
||||
return password
|
||||
}
|
||||
} else if host.Password != "" { // to be removed in a future version in favor of URL
|
||||
return host.Password
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (host *Host) Port() uint64 {
|
||||
var portString string
|
||||
if host.URL != "" {
|
||||
u, err := url.Parse(host.URL)
|
||||
if err != nil {
|
||||
goto defaultPort
|
||||
}
|
||||
portString = u.Port()
|
||||
} else if host.Addr != "" { // to be removed in a future version in favor of URL
|
||||
portString = strings.Split(host.Addr, ":")[1]
|
||||
}
|
||||
if portString != "" {
|
||||
port, err := strconv.ParseUint(portString, 10, 64)
|
||||
if err != nil {
|
||||
goto defaultPort
|
||||
}
|
||||
return port
|
||||
}
|
||||
defaultPort:
|
||||
switch host.Scheme() {
|
||||
case BastionSchemeSSH:
|
||||
return 22
|
||||
case BastionSchemeTelnet:
|
||||
return 23
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
func HostsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Groups").Preload("SSHKey")
|
||||
}
|
||||
func HostsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
||||
}
|
||||
func HostByName(db *gorm.DB, name string) (*Host, error) {
|
||||
var host Host
|
||||
db.Preload("SSHKey").Where("name = ?", name).Find(&host)
|
||||
if host.Name == "" {
|
||||
// FIXME: add available hosts
|
||||
return nil, fmt.Errorf("no such target: %q", name)
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
func (host *Host) ClientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, error) {
|
||||
config := gossh.ClientConfig{
|
||||
User: host.Username(),
|
||||
HostKeyCallback: hk,
|
||||
Auth: []gossh.AuthMethod{},
|
||||
}
|
||||
if host.SSHKey != nil {
|
||||
signer, err := gossh.ParsePrivateKey([]byte(host.SSHKey.PrivKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Auth = append(config.Auth, gossh.PublicKeys(signer))
|
||||
}
|
||||
if host.Passwd() != "" {
|
||||
config.Auth = append(config.Auth, gossh.Password(host.Passwd()))
|
||||
}
|
||||
if len(config.Auth) == 0 {
|
||||
return nil, fmt.Errorf("no valid authentication method for host %q", host.Name)
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// SSHKey helpers
|
||||
|
||||
func SSHKeysPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Hosts")
|
||||
}
|
||||
func SSHKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// HostGroup helpers
|
||||
|
||||
func HostGroupsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ACLs").Preload("Hosts")
|
||||
}
|
||||
func HostGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// UserGroup helpers
|
||||
|
||||
func UserGroupsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ACLs").Preload("Users")
|
||||
}
|
||||
func UserGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// User helpers
|
||||
|
||||
func UsersPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Groups").Preload("Keys").Preload("Roles")
|
||||
}
|
||||
func UsersByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers).Or("email IN (?)", identifiers).Or("name IN (?)", identifiers)
|
||||
}
|
||||
func (u *User) HasRole(name string) bool {
|
||||
for _, role := range u.Roles {
|
||||
if role.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func (u *User) CheckRoles(names []string) error {
|
||||
for _, name := range names {
|
||||
if u.HasRole(name) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("you don't have permission to access this feature (requires any of these roles: '%s')", strings.Join(names, "', '"))
|
||||
}
|
||||
|
||||
// ACL helpers
|
||||
|
||||
func ACLsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("UserGroups").Preload("HostGroups")
|
||||
}
|
||||
func ACLsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// UserKey helpers
|
||||
|
||||
func UserKeysPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("User")
|
||||
}
|
||||
func UserKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// UserRole helpers
|
||||
|
||||
//func UserRolesPreload(db *gorm.DB) *gorm.DB {
|
||||
// return db.Preload("Users")
|
||||
//}
|
||||
func UserRolesByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// Session helpers
|
||||
|
||||
func SessionsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("User").Preload("Host")
|
||||
}
|
||||
func SessionsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// Events helpers
|
||||
|
||||
func EventsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Author")
|
||||
}
|
||||
func EventsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers)
|
||||
}
|
||||
|
||||
func NewEvent(domain, action string) *Event {
|
||||
return &Event{
|
||||
Domain: domain,
|
||||
Action: action,
|
||||
ArgsMap: map[string]interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Event) String() string {
|
||||
return fmt.Sprintf("%s %s %s %s", e.Domain, e.Action, e.Entity, string(e.Args))
|
||||
}
|
||||
|
||||
func (e *Event) Log(db *gorm.DB) {
|
||||
if len(e.ArgsMap) > 0 {
|
||||
var err error
|
||||
if e.Args, err = json.Marshal(e.ArgsMap); err != nil {
|
||||
log.Printf("error: %v", err)
|
||||
}
|
||||
}
|
||||
log.Printf("info: %s", e)
|
||||
if err := db.Create(e).Error; err != nil {
|
||||
log.Printf("warning: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Event) SetAuthor(user *User) *Event {
|
||||
//e.Author = user
|
||||
e.AuthorID = user.ID
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Event) SetArg(name string, value interface{}) *Event {
|
||||
e.ArgsMap[name] = value
|
||||
return e
|
||||
}
|
||||
99
proxy.go
@@ -1,99 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func proxy(s ssh.Session, host *Host) error {
|
||||
config, err := host.ClientConfig(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rconn, err := gossh.Dial("tcp", host.Addr, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rconn.Close()
|
||||
|
||||
rch, rreqs, err := rconn.OpenChannel("session", []byte{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("SSH Connectin established")
|
||||
return pipe(s.MaskedReqs(), rreqs, s, rch)
|
||||
}
|
||||
|
||||
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel) error {
|
||||
defer func() {
|
||||
lch.Close()
|
||||
rch.Close()
|
||||
}()
|
||||
|
||||
errch := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(lch, rch)
|
||||
errch <- errors.New("lch closed the connection")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(rch, lch)
|
||||
errch <- errors.New("rch closed the connection")
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case req := <-lreqs: // forward ssh requests from local to remote
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := rch.SendRequest(req.Type, req.WantReply, req.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Reply(b, nil)
|
||||
case req := <-rreqs: // forward ssh requests from remote to local
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := lch.SendRequest(req.Type, req.WantReply, req.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Reply(b, nil)
|
||||
case err := <-errch:
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (host *Host) ClientConfig(_ ssh.Session) (*gossh.ClientConfig, error) {
|
||||
config := gossh.ClientConfig{
|
||||
User: host.User,
|
||||
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
|
||||
Auth: []gossh.AuthMethod{},
|
||||
}
|
||||
if host.SSHKey != nil {
|
||||
signer, err := gossh.ParsePrivateKey([]byte(host.SSHKey.PrivKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Auth = append(config.Auth, gossh.PublicKeys(signer))
|
||||
}
|
||||
if host.Password != "" {
|
||||
config.Auth = append(config.Auth, gossh.Password(host.Password))
|
||||
}
|
||||
if len(config.Auth) == 0 {
|
||||
return nil, fmt.Errorf("no valid authentication method for host %q", host.Name)
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
6
renovate.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"groupName": "all"
|
||||
}
|
||||
134
server.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/urfave/cli"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"moul.io/sshportal/pkg/bastion"
|
||||
)
|
||||
|
||||
type serverConfig struct {
|
||||
aesKey string
|
||||
dbDriver, dbURL string
|
||||
logsLocation string
|
||||
bindAddr string
|
||||
debug, demo bool
|
||||
idleTimeout time.Duration
|
||||
}
|
||||
|
||||
func parseServerConfig(c *cli.Context) (*serverConfig, error) {
|
||||
ret := &serverConfig{
|
||||
aesKey: c.String("aes-key"),
|
||||
dbDriver: c.String("db-driver"),
|
||||
dbURL: c.String("db-conn"),
|
||||
bindAddr: c.String("bind-address"),
|
||||
debug: c.Bool("debug"),
|
||||
demo: c.Bool("demo"),
|
||||
logsLocation: c.String("logs-location"),
|
||||
idleTimeout: c.Duration("idle-timeout"),
|
||||
}
|
||||
switch len(ret.aesKey) {
|
||||
case 0, 16, 24, 32:
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid aes key size, should be 16 or 24, 32")
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func ensureLogDirectory(location string) error {
|
||||
// check for the logdir existence
|
||||
logsLocation, err := os.Stat(location)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return os.MkdirAll(location, os.ModeDir|os.FileMode(0750))
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !logsLocation.IsDir() {
|
||||
return fmt.Errorf("log directory cannot be created")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func server(c *serverConfig) (err error) {
|
||||
var db = (*gorm.DB)(nil)
|
||||
|
||||
// try to setup the local DB
|
||||
if db, err = gorm.Open(c.dbDriver, c.dbURL); err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
origErr := err
|
||||
err = db.Close()
|
||||
if origErr != nil {
|
||||
err = origErr
|
||||
}
|
||||
}()
|
||||
if err = db.DB().Ping(); err != nil {
|
||||
return
|
||||
}
|
||||
db.LogMode(c.debug)
|
||||
if err = bastion.DBInit(db); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// create TCP listening socket
|
||||
ln, err := net.Listen("tcp", c.bindAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// configure server
|
||||
srv := &ssh.Server{
|
||||
Addr: c.bindAddr,
|
||||
Handler: func(s ssh.Session) { bastion.ShellHandler(s, Version, GitSha, GitTag, GitBranch) }, // ssh.Server.Handler is the handler for the DefaultSessionHandler
|
||||
Version: fmt.Sprintf("sshportal-%s", Version),
|
||||
ChannelHandlers: map[string]ssh.ChannelHandler{
|
||||
"default": bastion.ChannelHandler,
|
||||
},
|
||||
}
|
||||
|
||||
// configure channel handler
|
||||
bastion.DefaultChannelHandler = func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
|
||||
switch newChan.ChannelType() {
|
||||
case "session":
|
||||
go ssh.DefaultSessionHandler(srv, conn, newChan, ctx)
|
||||
case "direct-tcpip":
|
||||
go ssh.DirectTCPIPHandler(srv, conn, newChan, ctx)
|
||||
default:
|
||||
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
|
||||
log.Printf("failed to reject chan: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.idleTimeout != 0 {
|
||||
srv.IdleTimeout = c.idleTimeout
|
||||
// gliderlabs/ssh requires MaxTimeout to be non-zero if we want to use IdleTimeout.
|
||||
// So, set it to the max value, because we don't want a max timeout.
|
||||
srv.MaxTimeout = math.MaxInt64
|
||||
}
|
||||
|
||||
for _, opt := range []ssh.Option{
|
||||
// custom PublicKeyAuth handler
|
||||
ssh.PublicKeyAuth(bastion.PublicKeyAuthHandler(db, c.logsLocation, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
|
||||
ssh.PasswordAuth(bastion.PasswordAuthHandler(db, c.logsLocation, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
|
||||
// retrieve sshportal SSH private key from database
|
||||
bastion.PrivateKeyFromDB(db, c.aesKey),
|
||||
} {
|
||||
if err := srv.SetOption(opt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("info: SSH Server accepting connections on %s, idle-timout=%v", c.bindAddr, c.idleTimeout)
|
||||
return srv.Serve(ln)
|
||||
}
|
||||
80
testserver.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/kr/pty"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// testServer is an hidden handler used for integration tests
|
||||
func testServer(c *cli.Context) error {
|
||||
ssh.Handle(func(s ssh.Session) {
|
||||
helloMsg := struct {
|
||||
User string
|
||||
Environ []string
|
||||
Command []string
|
||||
}{
|
||||
User: s.User(),
|
||||
Environ: s.Environ(),
|
||||
Command: s.Command(),
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(s).Encode(&helloMsg); err != nil {
|
||||
log.Fatalf("failed to write helloMsg: %v", err)
|
||||
}
|
||||
cmd := exec.Command(s.Command()[0], s.Command()[1:]...) // #nosec
|
||||
if s.Command() == nil {
|
||||
cmd = exec.Command("/bin/sh") // #nosec
|
||||
}
|
||||
ptyReq, winCh, isPty := s.Pty()
|
||||
var cmdErr error
|
||||
if isPty {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
|
||||
f, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
fmt.Fprintf(s, "failed to run command: %v\n", err) // #nosec
|
||||
_ = s.Exit(1) // #nosec
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
for win := range winCh {
|
||||
_, _, _ = syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ),
|
||||
uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(win.Height), uint16(win.Width), 0, 0}))) // #nosec
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
// stdin
|
||||
_, _ = io.Copy(f, s) // #nosec
|
||||
}()
|
||||
// stdout
|
||||
_, _ = io.Copy(s, f) // #nosec
|
||||
cmdErr = cmd.Wait()
|
||||
} else {
|
||||
//cmd.Stdin = s
|
||||
cmd.Stdout = s
|
||||
cmd.Stderr = s
|
||||
cmdErr = cmd.Run()
|
||||
}
|
||||
|
||||
if cmdErr != nil {
|
||||
if exitError, ok := cmdErr.(*exec.ExitError); ok {
|
||||
_ = s.Exit(exitError.Sys().(syscall.WaitStatus).ExitStatus()) // #nosec
|
||||
return
|
||||
}
|
||||
}
|
||||
_ = s.Exit(cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()) // #nosec
|
||||
})
|
||||
|
||||
log.Println("starting ssh server on port 2222...")
|
||||
return ssh.ListenAndServe(":2222", nil)
|
||||
}
|
||||
13
util.go
@@ -1,13 +0,0 @@
|
||||
package main
|
||||
|
||||
import "math/rand"
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
func RandStringBytes(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
20
vendor/github.com/anmitsu/go-shlex/LICENSE
generated
vendored
@@ -1,20 +0,0 @@
|
||||
Copyright (c) anmitsu <anmitsu.s@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
38
vendor/github.com/anmitsu/go-shlex/README.md
generated
vendored
@@ -1,38 +0,0 @@
|
||||
# go-shlex
|
||||
|
||||
go-shlex is a library to make a lexical analyzer like Unix shell for
|
||||
Go.
|
||||
|
||||
## Install
|
||||
|
||||
go get -u "github.com/anmitsu/go-shlex"
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/anmitsu/go-shlex"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd := `cp -Rdp "file name" 'file name2' dir\ name`
|
||||
words, err := shlex.Split(cmd, true)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, w := range words {
|
||||
fmt.Println(w)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
http://godoc.org/github.com/anmitsu/go-shlex
|
||||
|
||||
193
vendor/github.com/anmitsu/go-shlex/shlex.go
generated
vendored
@@ -1,193 +0,0 @@
|
||||
// Package shlex provides a simple lexical analysis like Unix shell.
|
||||
package shlex
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoClosing = errors.New("No closing quotation")
|
||||
ErrNoEscaped = errors.New("No escaped character")
|
||||
)
|
||||
|
||||
// Tokenizer is the interface that classifies a token according to
|
||||
// words, whitespaces, quotations, escapes and escaped quotations.
|
||||
type Tokenizer interface {
|
||||
IsWord(rune) bool
|
||||
IsWhitespace(rune) bool
|
||||
IsQuote(rune) bool
|
||||
IsEscape(rune) bool
|
||||
IsEscapedQuote(rune) bool
|
||||
}
|
||||
|
||||
// DefaultTokenizer implements a simple tokenizer like Unix shell.
|
||||
type DefaultTokenizer struct{}
|
||||
|
||||
func (t *DefaultTokenizer) IsWord(r rune) bool {
|
||||
return r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r)
|
||||
}
|
||||
func (t *DefaultTokenizer) IsQuote(r rune) bool {
|
||||
switch r {
|
||||
case '\'', '"':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
func (t *DefaultTokenizer) IsWhitespace(r rune) bool {
|
||||
return unicode.IsSpace(r)
|
||||
}
|
||||
func (t *DefaultTokenizer) IsEscape(r rune) bool {
|
||||
return r == '\\'
|
||||
}
|
||||
func (t *DefaultTokenizer) IsEscapedQuote(r rune) bool {
|
||||
return r == '"'
|
||||
}
|
||||
|
||||
// Lexer represents a lexical analyzer.
|
||||
type Lexer struct {
|
||||
reader *bufio.Reader
|
||||
tokenizer Tokenizer
|
||||
posix bool
|
||||
whitespacesplit bool
|
||||
}
|
||||
|
||||
// NewLexer creates a new Lexer reading from io.Reader. This Lexer
|
||||
// has a DefaultTokenizer according to posix and whitespacesplit
|
||||
// rules.
|
||||
func NewLexer(r io.Reader, posix, whitespacesplit bool) *Lexer {
|
||||
return &Lexer{
|
||||
reader: bufio.NewReader(r),
|
||||
tokenizer: &DefaultTokenizer{},
|
||||
posix: posix,
|
||||
whitespacesplit: whitespacesplit,
|
||||
}
|
||||
}
|
||||
|
||||
// NewLexerString creates a new Lexer reading from a string. This
|
||||
// Lexer has a DefaultTokenizer according to posix and whitespacesplit
|
||||
// rules.
|
||||
func NewLexerString(s string, posix, whitespacesplit bool) *Lexer {
|
||||
return NewLexer(strings.NewReader(s), posix, whitespacesplit)
|
||||
}
|
||||
|
||||
// Split splits a string according to posix or non-posix rules.
|
||||
func Split(s string, posix bool) ([]string, error) {
|
||||
return NewLexerString(s, posix, true).Split()
|
||||
}
|
||||
|
||||
// SetTokenizer sets a Tokenizer.
|
||||
func (l *Lexer) SetTokenizer(t Tokenizer) {
|
||||
l.tokenizer = t
|
||||
}
|
||||
|
||||
func (l *Lexer) Split() ([]string, error) {
|
||||
result := make([]string, 0)
|
||||
for {
|
||||
token, err := l.readToken()
|
||||
if token != "" {
|
||||
result = append(result, token)
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (l *Lexer) readToken() (string, error) {
|
||||
t := l.tokenizer
|
||||
token := ""
|
||||
quoted := false
|
||||
state := ' '
|
||||
escapedstate := ' '
|
||||
scanning:
|
||||
for {
|
||||
next, _, err := l.reader.ReadRune()
|
||||
if err != nil {
|
||||
if t.IsQuote(state) {
|
||||
return token, ErrNoClosing
|
||||
} else if t.IsEscape(state) {
|
||||
return token, ErrNoEscaped
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case t.IsWhitespace(state):
|
||||
switch {
|
||||
case t.IsWhitespace(next):
|
||||
break scanning
|
||||
case l.posix && t.IsEscape(next):
|
||||
escapedstate = 'a'
|
||||
state = next
|
||||
case t.IsWord(next):
|
||||
token += string(next)
|
||||
state = 'a'
|
||||
case t.IsQuote(next):
|
||||
if !l.posix {
|
||||
token += string(next)
|
||||
}
|
||||
state = next
|
||||
default:
|
||||
token = string(next)
|
||||
if l.whitespacesplit {
|
||||
state = 'a'
|
||||
} else if token != "" || (l.posix && quoted) {
|
||||
break scanning
|
||||
}
|
||||
}
|
||||
case t.IsQuote(state):
|
||||
quoted = true
|
||||
switch {
|
||||
case next == state:
|
||||
if !l.posix {
|
||||
token += string(next)
|
||||
break scanning
|
||||
} else {
|
||||
state = 'a'
|
||||
}
|
||||
case l.posix && t.IsEscape(next) && t.IsEscapedQuote(state):
|
||||
escapedstate = state
|
||||
state = next
|
||||
default:
|
||||
token += string(next)
|
||||
}
|
||||
case t.IsEscape(state):
|
||||
if t.IsQuote(escapedstate) && next != state && next != escapedstate {
|
||||
token += string(state)
|
||||
}
|
||||
token += string(next)
|
||||
state = escapedstate
|
||||
case t.IsWord(state):
|
||||
switch {
|
||||
case t.IsWhitespace(next):
|
||||
if token != "" || (l.posix && quoted) {
|
||||
break scanning
|
||||
}
|
||||
case l.posix && t.IsQuote(next):
|
||||
state = next
|
||||
case l.posix && t.IsEscape(next):
|
||||
escapedstate = 'a'
|
||||
state = next
|
||||
case t.IsWord(next) || t.IsQuote(next):
|
||||
token += string(next)
|
||||
default:
|
||||
if l.whitespacesplit {
|
||||
token += string(next)
|
||||
} else if token != "" {
|
||||
l.reader.UnreadRune()
|
||||
break scanning
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
27
vendor/github.com/gliderlabs/ssh/LICENSE
generated
vendored
@@ -1,27 +0,0 @@
|
||||
Copyright (c) 2016 Glider Labs. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Glider Labs nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
96
vendor/github.com/gliderlabs/ssh/README.md
generated
vendored
@@ -1,96 +0,0 @@
|
||||
# gliderlabs/ssh
|
||||
|
||||
[](https://godoc.org/github.com/gliderlabs/ssh)
|
||||
[](https://circleci.com/gh/gliderlabs/ssh)
|
||||
[](https://goreportcard.com/report/github.com/gliderlabs/ssh)
|
||||
[](#sponsors)
|
||||
[](http://slack.gliderlabs.com)
|
||||
[](https://app.convertkit.com/landing_pages/243312)
|
||||
|
||||
> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member
|
||||
|
||||
This Go package wraps the [crypto/ssh
|
||||
package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for
|
||||
building SSH servers. The goal of the API was to make it as simple as using
|
||||
[net/http](https://golang.org/pkg/net/http/), so the API is very similar:
|
||||
|
||||
```
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gliderlabs/ssh"
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ssh.Handle(func(s ssh.Session) {
|
||||
io.WriteString(s, "Hello world\n")
|
||||
})
|
||||
|
||||
log.Fatal(ssh.ListenAndServe(":2222", nil))
|
||||
}
|
||||
|
||||
```
|
||||
This package was built by [@progrium](https://twitter.com/progrium) after working on nearly a dozen projects at Glider Labs using SSH and collaborating with [@shazow](https://twitter.com/shazow) (known for [ssh-chat](https://github.com/shazow/ssh-chat)).
|
||||
|
||||
## Examples
|
||||
|
||||
A bunch of great examples are in the `_examples` directory.
|
||||
|
||||
## Usage
|
||||
|
||||
[See GoDoc reference.](https://godoc.org/github.com/gliderlabs/ssh)
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are welcome! However, since this project is very much about API
|
||||
design, please submit API changes as issues to discuss before submitting PRs.
|
||||
|
||||
Also, you can [join our Slack](http://slack.gliderlabs.com) to discuss as well.
|
||||
|
||||
## Roadmap
|
||||
|
||||
* Non-session channel handlers
|
||||
* Cleanup callback API
|
||||
* 1.0 release
|
||||
* High-level client?
|
||||
|
||||
## Sponsors
|
||||
|
||||
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/ssh#sponsor)]
|
||||
|
||||
<a href="https://opencollective.com/ssh/sponsor/0/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/1/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/2/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/3/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/4/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/5/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/6/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/7/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/8/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/9/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/10/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/10/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/11/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/11/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/12/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/12/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/13/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/13/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/14/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/14/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/15/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/15/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/16/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/16/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/17/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/17/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/18/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/18/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/19/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/19/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/20/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/20/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/21/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/21/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/22/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/22/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/23/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/23/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/24/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/24/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/25/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/25/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/26/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/26/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/27/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/27/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/28/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/28/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/29/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/29/avatar.svg"></a>
|
||||
|
||||
## License
|
||||
|
||||
BSD
|
||||
81
vendor/github.com/gliderlabs/ssh/agent.go
generated
vendored
@@ -1,81 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
agentRequestType = "auth-agent-req@openssh.com"
|
||||
agentChannelType = "auth-agent@openssh.com"
|
||||
|
||||
agentTempDir = "auth-agent"
|
||||
agentListenFile = "listener.sock"
|
||||
)
|
||||
|
||||
// contextKeyAgentRequest is an internal context key for storing if the
|
||||
// client requested agent forwarding
|
||||
var contextKeyAgentRequest = &contextKey{"auth-agent-req"}
|
||||
|
||||
func setAgentRequested(sess *session) {
|
||||
sess.ctx.SetValue(contextKeyAgentRequest, true)
|
||||
}
|
||||
|
||||
// AgentRequested returns true if the client requested agent forwarding.
|
||||
func AgentRequested(sess Session) bool {
|
||||
return sess.Context().Value(contextKeyAgentRequest) == true
|
||||
}
|
||||
|
||||
// NewAgentListener sets up a temporary Unix socket that can be communicated
|
||||
// to the session environment and used for forwarding connections.
|
||||
func NewAgentListener() (net.Listener, error) {
|
||||
dir, err := ioutil.TempDir("", agentTempDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l, err := net.Listen("unix", path.Join(dir, agentListenFile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ForwardAgentConnections takes connections from a listener to proxy into the
|
||||
// session on the OpenSSH channel for agent connections. It blocks and services
|
||||
// connections until the listener stop accepting.
|
||||
func ForwardAgentConnections(l net.Listener, s Session) {
|
||||
sshConn := s.Context().Value(ContextKeyConn).(gossh.Conn)
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
channel, reqs, err := sshConn.OpenChannel(agentChannelType, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer channel.Close()
|
||||
go gossh.DiscardRequests(reqs)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
io.Copy(conn, channel)
|
||||
conn.(*net.UnixConn).CloseWrite()
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
io.Copy(channel, conn)
|
||||
channel.CloseWrite()
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
10
vendor/github.com/gliderlabs/ssh/circle.yml
generated
vendored
@@ -1,10 +0,0 @@
|
||||
version: 2.0
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: golang:1.8
|
||||
working_directory: /go/src/github.com/gliderlabs/ssh
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get
|
||||
- run: go test -v -race
|
||||
55
vendor/github.com/gliderlabs/ssh/conn.go
generated
vendored
@@ -1,55 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type serverConn struct {
|
||||
net.Conn
|
||||
|
||||
idleTimeout time.Duration
|
||||
maxDeadline time.Time
|
||||
closeCanceler context.CancelFunc
|
||||
}
|
||||
|
||||
func (c *serverConn) Write(p []byte) (n int, err error) {
|
||||
c.updateDeadline()
|
||||
n, err = c.Conn.Write(p)
|
||||
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
|
||||
c.closeCanceler()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *serverConn) Read(b []byte) (n int, err error) {
|
||||
c.updateDeadline()
|
||||
n, err = c.Conn.Read(b)
|
||||
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
|
||||
c.closeCanceler()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *serverConn) Close() (err error) {
|
||||
err = c.Conn.Close()
|
||||
if c.closeCanceler != nil {
|
||||
c.closeCanceler()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *serverConn) updateDeadline() {
|
||||
switch {
|
||||
case c.idleTimeout > 0:
|
||||
idleDeadline := time.Now().Add(c.idleTimeout)
|
||||
if idleDeadline.Unix() < c.maxDeadline.Unix() {
|
||||
c.Conn.SetDeadline(idleDeadline)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
c.Conn.SetDeadline(c.maxDeadline)
|
||||
}
|
||||
}
|
||||
148
vendor/github.com/gliderlabs/ssh/context.go
generated
vendored
@@ -1,148 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// contextKey is a value for use with context.WithValue. It's used as
|
||||
// a pointer so it fits in an interface{} without allocation.
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var (
|
||||
// ContextKeyUser is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type string.
|
||||
ContextKeyUser = &contextKey{"user"}
|
||||
|
||||
// ContextKeySessionID is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type string.
|
||||
ContextKeySessionID = &contextKey{"session-id"}
|
||||
|
||||
// ContextKeyPermissions is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type *Permissions.
|
||||
ContextKeyPermissions = &contextKey{"permissions"}
|
||||
|
||||
// ContextKeyClientVersion is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type string.
|
||||
ContextKeyClientVersion = &contextKey{"client-version"}
|
||||
|
||||
// ContextKeyServerVersion is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type string.
|
||||
ContextKeyServerVersion = &contextKey{"server-version"}
|
||||
|
||||
// ContextKeyLocalAddr is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type net.Addr.
|
||||
ContextKeyLocalAddr = &contextKey{"local-addr"}
|
||||
|
||||
// ContextKeyRemoteAddr is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type net.Addr.
|
||||
ContextKeyRemoteAddr = &contextKey{"remote-addr"}
|
||||
|
||||
// ContextKeyServer is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type *Server.
|
||||
ContextKeyServer = &contextKey{"ssh-server"}
|
||||
|
||||
// ContextKeyConn is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type gossh.Conn.
|
||||
ContextKeyConn = &contextKey{"ssh-conn"}
|
||||
|
||||
// ContextKeyPublicKey is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type PublicKey.
|
||||
ContextKeyPublicKey = &contextKey{"public-key"}
|
||||
)
|
||||
|
||||
// Context is a package specific context interface. It exposes connection
|
||||
// metadata and allows new values to be easily written to it. It's used in
|
||||
// authentication handlers and callbacks, and its underlying context.Context is
|
||||
// exposed on Session in the session Handler.
|
||||
type Context interface {
|
||||
context.Context
|
||||
|
||||
// User returns the username used when establishing the SSH connection.
|
||||
User() string
|
||||
|
||||
// SessionID returns the session hash.
|
||||
SessionID() string
|
||||
|
||||
// ClientVersion returns the version reported by the client.
|
||||
ClientVersion() string
|
||||
|
||||
// ServerVersion returns the version reported by the server.
|
||||
ServerVersion() string
|
||||
|
||||
// RemoteAddr returns the remote address for this connection.
|
||||
RemoteAddr() net.Addr
|
||||
|
||||
// LocalAddr returns the local address for this connection.
|
||||
LocalAddr() net.Addr
|
||||
|
||||
// Permissions returns the Permissions object used for this connection.
|
||||
Permissions() *Permissions
|
||||
|
||||
// SetValue allows you to easily write new values into the underlying context.
|
||||
SetValue(key, value interface{})
|
||||
}
|
||||
|
||||
type sshContext struct {
|
||||
context.Context
|
||||
}
|
||||
|
||||
func newContext(srv *Server) (*sshContext, context.CancelFunc) {
|
||||
innerCtx, cancel := context.WithCancel(context.Background())
|
||||
ctx := &sshContext{innerCtx}
|
||||
ctx.SetValue(ContextKeyServer, srv)
|
||||
perms := &Permissions{&gossh.Permissions{}}
|
||||
ctx.SetValue(ContextKeyPermissions, perms)
|
||||
return ctx, cancel
|
||||
}
|
||||
|
||||
// this is separate from newContext because we will get ConnMetadata
|
||||
// at different points so it needs to be applied separately
|
||||
func (ctx *sshContext) applyConnMetadata(conn gossh.ConnMetadata) {
|
||||
if ctx.Value(ContextKeySessionID) != nil {
|
||||
return
|
||||
}
|
||||
ctx.SetValue(ContextKeySessionID, hex.EncodeToString(conn.SessionID()))
|
||||
ctx.SetValue(ContextKeyClientVersion, string(conn.ClientVersion()))
|
||||
ctx.SetValue(ContextKeyServerVersion, string(conn.ServerVersion()))
|
||||
ctx.SetValue(ContextKeyUser, conn.User())
|
||||
ctx.SetValue(ContextKeyLocalAddr, conn.LocalAddr())
|
||||
ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr())
|
||||
}
|
||||
|
||||
func (ctx *sshContext) SetValue(key, value interface{}) {
|
||||
ctx.Context = context.WithValue(ctx.Context, key, value)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) User() string {
|
||||
return ctx.Value(ContextKeyUser).(string)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) SessionID() string {
|
||||
return ctx.Value(ContextKeySessionID).(string)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) ClientVersion() string {
|
||||
return ctx.Value(ContextKeyClientVersion).(string)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) ServerVersion() string {
|
||||
return ctx.Value(ContextKeyServerVersion).(string)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) RemoteAddr() net.Addr {
|
||||
return ctx.Value(ContextKeyRemoteAddr).(net.Addr)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) LocalAddr() net.Addr {
|
||||
return ctx.Value(ContextKeyLocalAddr).(net.Addr)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) Permissions() *Permissions {
|
||||
return ctx.Value(ContextKeyPermissions).(*Permissions)
|
||||
}
|
||||
47
vendor/github.com/gliderlabs/ssh/doc.go
generated
vendored
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
|
||||
Package ssh wraps the crypto/ssh package with a higher-level API for building
|
||||
SSH servers. The goal of the API was to make it as simple as using net/http, so
|
||||
the API is very similar.
|
||||
|
||||
You should be able to build any SSH server using only this package, which wraps
|
||||
relevant types and some functions from crypto/ssh. However, you still need to
|
||||
use crypto/ssh for building SSH clients.
|
||||
|
||||
ListenAndServe starts an SSH server with a given address, handler, and options. The
|
||||
handler is usually nil, which means to use DefaultHandler. Handle sets DefaultHandler:
|
||||
|
||||
ssh.Handle(func(s ssh.Session) {
|
||||
io.WriteString(s, "Hello world\n")
|
||||
})
|
||||
|
||||
log.Fatal(ssh.ListenAndServe(":2222", nil))
|
||||
|
||||
If you don't specify a host key, it will generate one every time. This is convenient
|
||||
except you'll have to deal with clients being confused that the host key is different.
|
||||
It's a better idea to generate or point to an existing key on your system:
|
||||
|
||||
log.Fatal(ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile("/Users/progrium/.ssh/id_rsa")))
|
||||
|
||||
Although all options have functional option helpers, another way to control the
|
||||
server's behavior is by creating a custom Server:
|
||||
|
||||
s := &ssh.Server{
|
||||
Addr: ":2222",
|
||||
Handler: sessionHandler,
|
||||
PublicKeyHandler: authHandler,
|
||||
}
|
||||
s.AddHostKey(hostKeySigner)
|
||||
|
||||
log.Fatal(s.ListenAndServe())
|
||||
|
||||
This package automatically handles basic SSH requests like setting environment
|
||||
variables, requesting PTY, and changing window size. These requests are
|
||||
processed, responded to, and any relevant state is updated. This state is then
|
||||
exposed to you via the Session interface.
|
||||
|
||||
The one big feature missing from the Session abstraction is signals. This was
|
||||
started, but not completed. Pull Requests welcome!
|
||||
|
||||
*/
|
||||
package ssh
|
||||
77
vendor/github.com/gliderlabs/ssh/options.go
generated
vendored
@@ -1,77 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// PasswordAuth returns a functional option that sets PasswordHandler on the server.
|
||||
func PasswordAuth(fn PasswordHandler) Option {
|
||||
return func(srv *Server) error {
|
||||
srv.PasswordHandler = fn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PublicKeyAuth returns a functional option that sets PublicKeyHandler on the server.
|
||||
func PublicKeyAuth(fn PublicKeyHandler) Option {
|
||||
return func(srv *Server) error {
|
||||
srv.PublicKeyHandler = fn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// HostKeyFile returns a functional option that adds HostSigners to the server
|
||||
// from a PEM file at filepath.
|
||||
func HostKeyFile(filepath string) Option {
|
||||
return func(srv *Server) error {
|
||||
pemBytes, err := ioutil.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signer, err := gossh.ParsePrivateKey(pemBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srv.AddHostKey(signer)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// HostKeyPEM returns a functional option that adds HostSigners to the server
|
||||
// from a PEM file as bytes.
|
||||
func HostKeyPEM(bytes []byte) Option {
|
||||
return func(srv *Server) error {
|
||||
signer, err := gossh.ParsePrivateKey(bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srv.AddHostKey(signer)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NoPty returns a functional option that sets PtyCallback to return false,
|
||||
// denying PTY requests.
|
||||
func NoPty() Option {
|
||||
return func(srv *Server) error {
|
||||
srv.PtyCallback = func(ctx Context, pty Pty) bool {
|
||||
return false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WrapConn returns a functional option that sets ConnCallback on the server.
|
||||
func WrapConn(fn ConnCallback) Option {
|
||||
return func(srv *Server) error {
|
||||
srv.ConnCallback = fn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
332
vendor/github.com/gliderlabs/ssh/server.go
generated
vendored
@@ -1,332 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// ErrServerClosed is returned by the Server's Serve, ListenAndServe,
|
||||
// and ListenAndServeTLS methods after a call to Shutdown or Close.
|
||||
var ErrServerClosed = errors.New("ssh: Server closed")
|
||||
|
||||
// Server defines parameters for running an SSH server. The zero value for
|
||||
// Server is a valid configuration. When both PasswordHandler and
|
||||
// PublicKeyHandler are nil, no client authentication is performed.
|
||||
type Server struct {
|
||||
Addr string // TCP address to listen on, ":22" if empty
|
||||
Handler Handler // handler to invoke, ssh.DefaultHandler if nil
|
||||
HostSigners []Signer // private keys for the host key, must have at least one
|
||||
Version string // server version to be sent before the initial handshake
|
||||
|
||||
PasswordHandler PasswordHandler // password authentication handler
|
||||
PublicKeyHandler PublicKeyHandler // public key authentication handler
|
||||
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
|
||||
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
|
||||
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
|
||||
|
||||
IdleTimeout time.Duration // connection timeout when no activity, none if empty
|
||||
MaxTimeout time.Duration // absolute connection timeout, none if empty
|
||||
|
||||
channelHandlers map[string]channelHandler
|
||||
|
||||
mu sync.Mutex
|
||||
listeners map[net.Listener]struct{}
|
||||
conns map[*gossh.ServerConn]struct{}
|
||||
doneChan chan struct{}
|
||||
}
|
||||
|
||||
// internal for now
|
||||
type channelHandler func(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx *sshContext)
|
||||
|
||||
func (srv *Server) ensureHostSigner() error {
|
||||
if len(srv.HostSigners) == 0 {
|
||||
signer, err := generateSigner()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.HostSigners = append(srv.HostSigners, signer)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *Server) config(ctx *sshContext) *gossh.ServerConfig {
|
||||
srv.channelHandlers = map[string]channelHandler{
|
||||
"session": sessionHandler,
|
||||
"direct-tcpip": directTcpipHandler,
|
||||
}
|
||||
config := &gossh.ServerConfig{}
|
||||
for _, signer := range srv.HostSigners {
|
||||
config.AddHostKey(signer)
|
||||
}
|
||||
if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil {
|
||||
config.NoClientAuth = true
|
||||
}
|
||||
if srv.Version != "" {
|
||||
config.ServerVersion = "SSH-2.0-" + srv.Version
|
||||
}
|
||||
if srv.PasswordHandler != nil {
|
||||
config.PasswordCallback = func(conn gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) {
|
||||
ctx.applyConnMetadata(conn)
|
||||
if ok := srv.PasswordHandler(ctx, string(password)); !ok {
|
||||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
|
||||
}
|
||||
return ctx.Permissions().Permissions, nil
|
||||
}
|
||||
}
|
||||
if srv.PublicKeyHandler != nil {
|
||||
config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
|
||||
ctx.applyConnMetadata(conn)
|
||||
if ok := srv.PublicKeyHandler(ctx, key); !ok {
|
||||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
|
||||
}
|
||||
ctx.SetValue(ContextKeyPublicKey, key)
|
||||
return ctx.Permissions().Permissions, nil
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// Handle sets the Handler for the server.
|
||||
func (srv *Server) Handle(fn Handler) {
|
||||
srv.Handler = fn
|
||||
}
|
||||
|
||||
// Close immediately closes all active listeners and all active
|
||||
// connections.
|
||||
//
|
||||
// Close returns any error returned from closing the Server's
|
||||
// underlying Listener(s).
|
||||
func (srv *Server) Close() error {
|
||||
srv.mu.Lock()
|
||||
defer srv.mu.Unlock()
|
||||
srv.closeDoneChanLocked()
|
||||
err := srv.closeListenersLocked()
|
||||
for c := range srv.conns {
|
||||
c.Close()
|
||||
delete(srv.conns, c)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// shutdownPollInterval is how often we poll for quiescence
|
||||
// during Server.Shutdown. This is lower during tests, to
|
||||
// speed up tests.
|
||||
// Ideally we could find a solution that doesn't involve polling,
|
||||
// but which also doesn't have a high runtime cost (and doesn't
|
||||
// involve any contentious mutexes), but that is left as an
|
||||
// exercise for the reader.
|
||||
var shutdownPollInterval = 500 * time.Millisecond
|
||||
|
||||
// Shutdown gracefully shuts down the server without interrupting any
|
||||
// active connections. Shutdown works by first closing all open
|
||||
// listeners, and then waiting indefinitely for connections to close.
|
||||
// If the provided context expires before the shutdown is complete,
|
||||
// then the context's error is returned.
|
||||
func (srv *Server) Shutdown(ctx context.Context) error {
|
||||
srv.mu.Lock()
|
||||
lnerr := srv.closeListenersLocked()
|
||||
srv.closeDoneChanLocked()
|
||||
srv.mu.Unlock()
|
||||
ticker := time.NewTicker(shutdownPollInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
srv.mu.Lock()
|
||||
conns := len(srv.conns)
|
||||
srv.mu.Unlock()
|
||||
if conns == 0 {
|
||||
return lnerr
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Serve accepts incoming connections on the Listener l, creating a new
|
||||
// connection goroutine for each. The connection goroutines read requests and then
|
||||
// calls srv.Handler to handle sessions.
|
||||
//
|
||||
// Serve always returns a non-nil error.
|
||||
func (srv *Server) Serve(l net.Listener) error {
|
||||
defer l.Close()
|
||||
if err := srv.ensureHostSigner(); err != nil {
|
||||
return err
|
||||
}
|
||||
if srv.Handler == nil {
|
||||
srv.Handler = DefaultHandler
|
||||
}
|
||||
var tempDelay time.Duration
|
||||
|
||||
srv.trackListener(l, true)
|
||||
defer srv.trackListener(l, false)
|
||||
for {
|
||||
conn, e := l.Accept()
|
||||
if e != nil {
|
||||
select {
|
||||
case <-srv.getDoneChan():
|
||||
return ErrServerClosed
|
||||
default:
|
||||
}
|
||||
if ne, ok := e.(net.Error); ok && ne.Temporary() {
|
||||
if tempDelay == 0 {
|
||||
tempDelay = 5 * time.Millisecond
|
||||
} else {
|
||||
tempDelay *= 2
|
||||
}
|
||||
if max := 1 * time.Second; tempDelay > max {
|
||||
tempDelay = max
|
||||
}
|
||||
time.Sleep(tempDelay)
|
||||
continue
|
||||
}
|
||||
return e
|
||||
}
|
||||
go srv.handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) handleConn(newConn net.Conn) {
|
||||
if srv.ConnCallback != nil {
|
||||
cbConn := srv.ConnCallback(newConn)
|
||||
if cbConn == nil {
|
||||
newConn.Close()
|
||||
return
|
||||
}
|
||||
newConn = cbConn
|
||||
}
|
||||
ctx, cancel := newContext(srv)
|
||||
conn := &serverConn{
|
||||
Conn: newConn,
|
||||
idleTimeout: srv.IdleTimeout,
|
||||
closeCanceler: cancel,
|
||||
}
|
||||
if srv.MaxTimeout > 0 {
|
||||
conn.maxDeadline = time.Now().Add(srv.MaxTimeout)
|
||||
}
|
||||
defer conn.Close()
|
||||
sshConn, chans, reqs, err := gossh.NewServerConn(conn, srv.config(ctx))
|
||||
if err != nil {
|
||||
// TODO: trigger event callback
|
||||
return
|
||||
}
|
||||
|
||||
srv.trackConn(sshConn, true)
|
||||
defer srv.trackConn(sshConn, false)
|
||||
|
||||
ctx.SetValue(ContextKeyConn, sshConn)
|
||||
ctx.applyConnMetadata(sshConn)
|
||||
go gossh.DiscardRequests(reqs)
|
||||
for ch := range chans {
|
||||
handler, found := srv.channelHandlers[ch.ChannelType()]
|
||||
if !found {
|
||||
ch.Reject(gossh.UnknownChannelType, "unsupported channel type")
|
||||
continue
|
||||
}
|
||||
go handler(srv, sshConn, ch, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// ListenAndServe listens on the TCP network address srv.Addr and then calls
|
||||
// Serve to handle incoming connections. If srv.Addr is blank, ":22" is used.
|
||||
// ListenAndServe always returns a non-nil error.
|
||||
func (srv *Server) ListenAndServe() error {
|
||||
addr := srv.Addr
|
||||
if addr == "" {
|
||||
addr = ":22"
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.Serve(ln)
|
||||
}
|
||||
|
||||
// AddHostKey adds a private key as a host key. If an existing host key exists
|
||||
// with the same algorithm, it is overwritten. Each server config must have at
|
||||
// least one host key.
|
||||
func (srv *Server) AddHostKey(key Signer) {
|
||||
// these are later added via AddHostKey on ServerConfig, which performs the
|
||||
// check for one of every algorithm.
|
||||
srv.HostSigners = append(srv.HostSigners, key)
|
||||
}
|
||||
|
||||
// SetOption runs a functional option against the server.
|
||||
func (srv *Server) SetOption(option Option) error {
|
||||
return option(srv)
|
||||
}
|
||||
|
||||
func (srv *Server) getDoneChan() <-chan struct{} {
|
||||
srv.mu.Lock()
|
||||
defer srv.mu.Unlock()
|
||||
return srv.getDoneChanLocked()
|
||||
}
|
||||
|
||||
func (srv *Server) getDoneChanLocked() chan struct{} {
|
||||
if srv.doneChan == nil {
|
||||
srv.doneChan = make(chan struct{})
|
||||
}
|
||||
return srv.doneChan
|
||||
}
|
||||
|
||||
func (srv *Server) closeDoneChanLocked() {
|
||||
ch := srv.getDoneChanLocked()
|
||||
select {
|
||||
case <-ch:
|
||||
// Already closed. Don't close again.
|
||||
default:
|
||||
// Safe to close here. We're the only closer, guarded
|
||||
// by srv.mu.
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) closeListenersLocked() error {
|
||||
var err error
|
||||
for ln := range srv.listeners {
|
||||
if cerr := ln.Close(); cerr != nil && err == nil {
|
||||
err = cerr
|
||||
}
|
||||
delete(srv.listeners, ln)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (srv *Server) trackListener(ln net.Listener, add bool) {
|
||||
srv.mu.Lock()
|
||||
defer srv.mu.Unlock()
|
||||
if srv.listeners == nil {
|
||||
srv.listeners = make(map[net.Listener]struct{})
|
||||
}
|
||||
if add {
|
||||
// If the *Server is being reused after a previous
|
||||
// Close or Shutdown, reset its doneChan:
|
||||
if len(srv.listeners) == 0 && len(srv.conns) == 0 {
|
||||
srv.doneChan = nil
|
||||
}
|
||||
srv.listeners[ln] = struct{}{}
|
||||
} else {
|
||||
delete(srv.listeners, ln)
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) trackConn(c *gossh.ServerConn, add bool) {
|
||||
srv.mu.Lock()
|
||||
defer srv.mu.Unlock()
|
||||
if srv.conns == nil {
|
||||
srv.conns = make(map[*gossh.ServerConn]struct{})
|
||||
}
|
||||
if add {
|
||||
srv.conns[c] = struct{}{}
|
||||
} else {
|
||||
delete(srv.conns, c)
|
||||
}
|
||||
}
|
||||
301
vendor/github.com/gliderlabs/ssh/session.go
generated
vendored
@@ -1,301 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/anmitsu/go-shlex"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// Session provides access to information about an SSH session and methods
|
||||
// to read and write to the SSH channel with an embedded Channel interface from
|
||||
// cypto/ssh.
|
||||
//
|
||||
// When Command() returns an empty slice, the user requested a shell. Otherwise
|
||||
// the user is performing an exec with those command arguments.
|
||||
//
|
||||
// TODO: Signals
|
||||
type Session interface {
|
||||
gossh.Channel
|
||||
|
||||
// User returns the username used when establishing the SSH connection.
|
||||
User() string
|
||||
|
||||
// RemoteAddr returns the net.Addr of the client side of the connection.
|
||||
RemoteAddr() net.Addr
|
||||
|
||||
// LocalAddr returns the net.Addr of the server side of the connection.
|
||||
LocalAddr() net.Addr
|
||||
|
||||
// Environ returns a copy of strings representing the environment set by the
|
||||
// user for this session, in the form "key=value".
|
||||
Environ() []string
|
||||
|
||||
// Exit sends an exit status and then closes the session.
|
||||
Exit(code int) error
|
||||
|
||||
// Command returns a shell parsed slice of arguments that were provided by the
|
||||
// user. Shell parsing splits the command string according to POSIX shell rules,
|
||||
// which considers quoting not just whitespace.
|
||||
Command() []string
|
||||
|
||||
// PublicKey returns the PublicKey used to authenticate. If a public key was not
|
||||
// used it will return nil.
|
||||
PublicKey() PublicKey
|
||||
|
||||
// Context returns the connection's context. The returned context is always
|
||||
// non-nil and holds the same data as the Context passed into auth
|
||||
// handlers and callbacks.
|
||||
//
|
||||
// The context is canceled when the client's connection closes or I/O
|
||||
// operation fails.
|
||||
Context() context.Context
|
||||
|
||||
// Permissions returns a copy of the Permissions object that was available for
|
||||
// setup in the auth handlers via the Context.
|
||||
Permissions() Permissions
|
||||
|
||||
// Pty returns PTY information, a channel of window size changes, and a boolean
|
||||
// of whether or not a PTY was accepted for this session.
|
||||
Pty() (Pty, <-chan Window, bool)
|
||||
|
||||
// Signals registers a channel to receive signals sent from the client. The
|
||||
// channel must handle signal sends or it will block the SSH request loop.
|
||||
// Registering nil will unregister the channel from signal sends. During the
|
||||
// time no channel is registered signals are buffered up to a reasonable amount.
|
||||
// If there are buffered signals when a channel is registered, they will be
|
||||
// sent in order on the channel immediately after registering.
|
||||
Signals(c chan<- Signal)
|
||||
|
||||
MaskedReqs() chan *gossh.Request
|
||||
}
|
||||
|
||||
// maxSigBufSize is how many signals will be buffered
|
||||
// when there is no signal channel specified
|
||||
const maxSigBufSize = 128
|
||||
|
||||
func sessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx *sshContext) {
|
||||
ch, reqs, err := newChan.Accept()
|
||||
if err != nil {
|
||||
// TODO: trigger event callback
|
||||
return
|
||||
}
|
||||
sess := &session{
|
||||
Channel: ch,
|
||||
conn: conn,
|
||||
handler: srv.Handler,
|
||||
ptyCb: srv.PtyCallback,
|
||||
maskedReqs: make(chan *gossh.Request, 5),
|
||||
ctx: ctx,
|
||||
}
|
||||
sess.handleRequests(reqs)
|
||||
}
|
||||
|
||||
type session struct {
|
||||
sync.Mutex
|
||||
gossh.Channel
|
||||
conn *gossh.ServerConn
|
||||
handler Handler
|
||||
handled bool
|
||||
exited bool
|
||||
pty *Pty
|
||||
winch chan Window
|
||||
env []string
|
||||
ptyCb PtyCallback
|
||||
cmd []string
|
||||
ctx *sshContext
|
||||
sigCh chan<- Signal
|
||||
sigBuf []Signal
|
||||
maskedReqs chan *gossh.Request
|
||||
}
|
||||
|
||||
func (sess *session) Write(p []byte) (n int, err error) {
|
||||
if sess.pty != nil {
|
||||
m := len(p)
|
||||
// normalize \n to \r\n when pty is accepted.
|
||||
// this is a hardcoded shortcut since we don't support terminal modes.
|
||||
p = bytes.Replace(p, []byte{'\n'}, []byte{'\r', '\n'}, -1)
|
||||
p = bytes.Replace(p, []byte{'\r', '\r', '\n'}, []byte{'\r', '\n'}, -1)
|
||||
n, err = sess.Channel.Write(p)
|
||||
if n > m {
|
||||
n = m
|
||||
}
|
||||
return
|
||||
}
|
||||
return sess.Channel.Write(p)
|
||||
}
|
||||
|
||||
func (sess *session) PublicKey() PublicKey {
|
||||
sessionkey := sess.ctx.Value(ContextKeyPublicKey)
|
||||
if sessionkey == nil {
|
||||
return nil
|
||||
}
|
||||
return sessionkey.(PublicKey)
|
||||
}
|
||||
|
||||
func (sess *session) Permissions() Permissions {
|
||||
// use context permissions because its properly
|
||||
// wrapped and easier to dereference
|
||||
perms := sess.ctx.Value(ContextKeyPermissions).(*Permissions)
|
||||
return *perms
|
||||
}
|
||||
|
||||
func (sess *session) Context() context.Context {
|
||||
return sess.ctx.Context
|
||||
}
|
||||
|
||||
func (sess *session) Exit(code int) error {
|
||||
sess.Lock()
|
||||
defer sess.Unlock()
|
||||
|
||||
if sess.exited {
|
||||
return errors.New("Session.Exit called multiple times")
|
||||
}
|
||||
sess.exited = true
|
||||
|
||||
status := struct{ Status uint32 }{uint32(code)}
|
||||
_, err := sess.SendRequest("exit-status", false, gossh.Marshal(&status))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
close(sess.maskedReqs)
|
||||
|
||||
return sess.Close()
|
||||
}
|
||||
|
||||
func (sess *session) User() string {
|
||||
return sess.conn.User()
|
||||
}
|
||||
|
||||
func (sess *session) RemoteAddr() net.Addr {
|
||||
return sess.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (sess *session) LocalAddr() net.Addr {
|
||||
return sess.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (sess *session) Environ() []string {
|
||||
return append([]string(nil), sess.env...)
|
||||
}
|
||||
|
||||
func (sess *session) Command() []string {
|
||||
return append([]string(nil), sess.cmd...)
|
||||
}
|
||||
|
||||
func (sess *session) Pty() (Pty, <-chan Window, bool) {
|
||||
if sess.pty != nil {
|
||||
return *sess.pty, sess.winch, true
|
||||
}
|
||||
return Pty{}, sess.winch, false
|
||||
}
|
||||
|
||||
func (sess *session) Signals(c chan<- Signal) {
|
||||
sess.Lock()
|
||||
defer sess.Unlock()
|
||||
sess.sigCh = c
|
||||
if len(sess.sigBuf) > 0 {
|
||||
go func() {
|
||||
for _, sig := range sess.sigBuf {
|
||||
sess.sigCh <- sig
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (sess *session) MaskedReqs() chan *gossh.Request {
|
||||
return sess.maskedReqs
|
||||
}
|
||||
|
||||
func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
|
||||
for req := range reqs {
|
||||
switch req.Type {
|
||||
case "shell", "exec":
|
||||
if sess.handled {
|
||||
req.Reply(false, nil)
|
||||
continue
|
||||
}
|
||||
sess.handled = true
|
||||
req.Reply(true, nil)
|
||||
|
||||
var payload = struct{ Value string }{}
|
||||
gossh.Unmarshal(req.Payload, &payload)
|
||||
sess.cmd, _ = shlex.Split(payload.Value, true)
|
||||
go func() {
|
||||
sess.handler(sess)
|
||||
sess.Exit(0)
|
||||
}()
|
||||
case "env":
|
||||
if sess.handled {
|
||||
req.Reply(false, nil)
|
||||
continue
|
||||
}
|
||||
var kv struct{ Key, Value string }
|
||||
gossh.Unmarshal(req.Payload, &kv)
|
||||
sess.env = append(sess.env, fmt.Sprintf("%s=%s", kv.Key, kv.Value))
|
||||
req.Reply(true, nil)
|
||||
case "signal":
|
||||
var payload struct{ Signal string }
|
||||
gossh.Unmarshal(req.Payload, &payload)
|
||||
sess.Lock()
|
||||
if sess.sigCh != nil {
|
||||
sess.sigCh <- Signal(payload.Signal)
|
||||
} else {
|
||||
if len(sess.sigBuf) < maxSigBufSize {
|
||||
sess.sigBuf = append(sess.sigBuf, Signal(payload.Signal))
|
||||
}
|
||||
}
|
||||
sess.Unlock()
|
||||
case "pty-req":
|
||||
if sess.handled || sess.pty != nil {
|
||||
req.Reply(false, nil)
|
||||
continue
|
||||
}
|
||||
ptyReq, ok := parsePtyRequest(req.Payload)
|
||||
if !ok {
|
||||
req.Reply(false, nil)
|
||||
continue
|
||||
}
|
||||
if sess.ptyCb != nil {
|
||||
ok := sess.ptyCb(sess.ctx, ptyReq)
|
||||
if !ok {
|
||||
req.Reply(false, nil)
|
||||
continue
|
||||
}
|
||||
}
|
||||
sess.pty = &ptyReq
|
||||
sess.winch = make(chan Window, 1)
|
||||
sess.winch <- ptyReq.Window
|
||||
defer func() {
|
||||
// when reqs is closed
|
||||
close(sess.winch)
|
||||
}()
|
||||
req.Reply(ok, nil)
|
||||
case "window-change":
|
||||
if sess.pty == nil {
|
||||
req.Reply(false, nil)
|
||||
continue
|
||||
}
|
||||
win, ok := parseWinchRequest(req.Payload)
|
||||
if ok {
|
||||
sess.pty.Window = win
|
||||
sess.winch <- win
|
||||
}
|
||||
req.Reply(ok, nil)
|
||||
case agentRequestType:
|
||||
// TODO: option/callback to allow agent forwarding
|
||||
setAgentRequested(sess)
|
||||
req.Reply(true, nil)
|
||||
default:
|
||||
// TODO: debug log
|
||||
}
|
||||
|
||||
sess.maskedReqs <- req
|
||||
}
|
||||
}
|
||||
109
vendor/github.com/gliderlabs/ssh/ssh.go
generated
vendored
@@ -1,109 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Signal string
|
||||
|
||||
// POSIX signals as listed in RFC 4254 Section 6.10.
|
||||
const (
|
||||
SIGABRT Signal = "ABRT"
|
||||
SIGALRM Signal = "ALRM"
|
||||
SIGFPE Signal = "FPE"
|
||||
SIGHUP Signal = "HUP"
|
||||
SIGILL Signal = "ILL"
|
||||
SIGINT Signal = "INT"
|
||||
SIGKILL Signal = "KILL"
|
||||
SIGPIPE Signal = "PIPE"
|
||||
SIGQUIT Signal = "QUIT"
|
||||
SIGSEGV Signal = "SEGV"
|
||||
SIGTERM Signal = "TERM"
|
||||
SIGUSR1 Signal = "USR1"
|
||||
SIGUSR2 Signal = "USR2"
|
||||
)
|
||||
|
||||
// DefaultHandler is the default Handler used by Serve.
|
||||
var DefaultHandler Handler
|
||||
|
||||
// Option is a functional option handler for Server.
|
||||
type Option func(*Server) error
|
||||
|
||||
// Handler is a callback for handling established SSH sessions.
|
||||
type Handler func(Session)
|
||||
|
||||
// PublicKeyHandler is a callback for performing public key authentication.
|
||||
type PublicKeyHandler func(ctx Context, key PublicKey) bool
|
||||
|
||||
// PasswordHandler is a callback for performing password authentication.
|
||||
type PasswordHandler func(ctx Context, password string) bool
|
||||
|
||||
// PtyCallback is a hook for allowing PTY sessions.
|
||||
type PtyCallback func(ctx Context, pty Pty) bool
|
||||
|
||||
// ConnCallback is a hook for new connections before handling.
|
||||
// It allows wrapping for timeouts and limiting by returning
|
||||
// the net.Conn that will be used as the underlying connection.
|
||||
type ConnCallback func(conn net.Conn) net.Conn
|
||||
|
||||
// LocalPortForwardingCallback is a hook for allowing port forwarding
|
||||
type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool
|
||||
|
||||
// Window represents the size of a PTY window.
|
||||
type Window struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
// Pty represents a PTY request and configuration.
|
||||
type Pty struct {
|
||||
Term string
|
||||
Window Window
|
||||
// HELP WANTED: terminal modes!
|
||||
}
|
||||
|
||||
// Serve accepts incoming SSH connections on the listener l, creating a new
|
||||
// connection goroutine for each. The connection goroutines read requests and
|
||||
// then calls handler to handle sessions. Handler is typically nil, in which
|
||||
// case the DefaultHandler is used.
|
||||
func Serve(l net.Listener, handler Handler, options ...Option) error {
|
||||
srv := &Server{Handler: handler}
|
||||
for _, option := range options {
|
||||
if err := srv.SetOption(option); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return srv.Serve(l)
|
||||
}
|
||||
|
||||
// ListenAndServe listens on the TCP network address addr and then calls Serve
|
||||
// with handler to handle sessions on incoming connections. Handler is typically
|
||||
// nil, in which case the DefaultHandler is used.
|
||||
func ListenAndServe(addr string, handler Handler, options ...Option) error {
|
||||
srv := &Server{Addr: addr, Handler: handler}
|
||||
for _, option := range options {
|
||||
if err := srv.SetOption(option); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return srv.ListenAndServe()
|
||||
}
|
||||
|
||||
// Handle registers the handler as the DefaultHandler.
|
||||
func Handle(handler Handler) {
|
||||
DefaultHandler = handler
|
||||
}
|
||||
|
||||
// KeysEqual is constant time compare of the keys to avoid timing attacks.
|
||||
func KeysEqual(ak, bk PublicKey) bool {
|
||||
|
||||
//avoid panic if one of the keys is nil, return false instead
|
||||
if ak == nil || bk == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
a := ak.Marshal()
|
||||
b := bk.Marshal()
|
||||
return (len(a) == len(b) && subtle.ConstantTimeCompare(a, b) == 1)
|
||||
}
|
||||
58
vendor/github.com/gliderlabs/ssh/tcpip.go
generated
vendored
@@ -1,58 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// direct-tcpip data struct as specified in RFC4254, Section 7.2
|
||||
type forwardData struct {
|
||||
DestinationHost string
|
||||
DestinationPort uint32
|
||||
|
||||
OriginatorHost string
|
||||
OriginatorPort uint32
|
||||
}
|
||||
|
||||
func directTcpipHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx *sshContext) {
|
||||
d := forwardData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestinationHost, d.DestinationPort) {
|
||||
newChan.Reject(gossh.Prohibited, "port forwarding is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
dest := fmt.Sprintf("%s:%d", d.DestinationHost, d.DestinationPort)
|
||||
|
||||
var dialer net.Dialer
|
||||
dconn, err := dialer.DialContext(ctx, "tcp", dest)
|
||||
if err != nil {
|
||||
newChan.Reject(gossh.ConnectionFailed, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ch, reqs, err := newChan.Accept()
|
||||
if err != nil {
|
||||
dconn.Close()
|
||||
return
|
||||
}
|
||||
go gossh.DiscardRequests(reqs)
|
||||
|
||||
go func() {
|
||||
defer ch.Close()
|
||||
defer dconn.Close()
|
||||
io.Copy(ch, dconn)
|
||||
}()
|
||||
go func() {
|
||||
defer ch.Close()
|
||||
defer dconn.Close()
|
||||
io.Copy(dconn, ch)
|
||||
}()
|
||||
}
|
||||
89
vendor/github.com/gliderlabs/ssh/util.go
generated
vendored
@@ -1,89 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/binary"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func generateSigner() (ssh.Signer, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 768)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ssh.NewSignerFromKey(key)
|
||||
}
|
||||
|
||||
func parsePtyRequest(s []byte) (pty Pty, ok bool) {
|
||||
term, s, ok := parseString(s)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
width32, s, ok := parseUint32(s)
|
||||
if width32 < 1 {
|
||||
ok = false
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
height32, _, ok := parseUint32(s)
|
||||
if height32 < 1 {
|
||||
ok = false
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
pty = Pty{
|
||||
Term: term,
|
||||
Window: Window{
|
||||
Width: int(width32),
|
||||
Height: int(height32),
|
||||
},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseWinchRequest(s []byte) (win Window, ok bool) {
|
||||
width32, s, ok := parseUint32(s)
|
||||
if width32 < 1 {
|
||||
ok = false
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
height32, _, ok := parseUint32(s)
|
||||
if height32 < 1 {
|
||||
ok = false
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
win = Window{
|
||||
Width: int(width32),
|
||||
Height: int(height32),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseString(in []byte) (out string, rest []byte, ok bool) {
|
||||
if len(in) < 4 {
|
||||
return
|
||||
}
|
||||
length := binary.BigEndian.Uint32(in)
|
||||
if uint32(len(in)) < 4+length {
|
||||
return
|
||||
}
|
||||
out = string(in[4 : 4+length])
|
||||
rest = in[4+length:]
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func parseUint32(in []byte) (uint32, []byte, bool) {
|
||||
if len(in) < 4 {
|
||||
return 0, nil, false
|
||||
}
|
||||
return binary.BigEndian.Uint32(in), in[4:], true
|
||||
}
|
||||
33
vendor/github.com/gliderlabs/ssh/wrap.go
generated
vendored
@@ -1,33 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import gossh "golang.org/x/crypto/ssh"
|
||||
|
||||
// PublicKey is an abstraction of different types of public keys.
|
||||
type PublicKey interface {
|
||||
gossh.PublicKey
|
||||
}
|
||||
|
||||
// The Permissions type holds fine-grained permissions that are specific to a
|
||||
// user or a specific authentication method for a user. Permissions, except for
|
||||
// "source-address", must be enforced in the server application layer, after
|
||||
// successful authentication.
|
||||
type Permissions struct {
|
||||
*gossh.Permissions
|
||||
}
|
||||
|
||||
// A Signer can create signatures that verify against a public key.
|
||||
type Signer interface {
|
||||
gossh.Signer
|
||||
}
|
||||
|
||||
// ParseAuthorizedKey parses a public key from an authorized_keys file used in
|
||||
// OpenSSH according to the sshd(8) manual page.
|
||||
func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) {
|
||||
return gossh.ParseAuthorizedKey(in)
|
||||
}
|
||||
|
||||
// ParsePublicKey parses an SSH public key formatted for use in
|
||||
// the SSH wire protocol according to RFC 4253, section 6.6.
|
||||
func ParsePublicKey(in []byte) (out PublicKey, err error) {
|
||||
return gossh.ParsePublicKey(in)
|
||||
}
|
||||
75
vendor/github.com/go-sql-driver/mysql/AUTHORS
generated
vendored
@@ -1,75 +0,0 @@
|
||||
# This is the official list of Go-MySQL-Driver authors for copyright purposes.
|
||||
|
||||
# If you are submitting a patch, please add your name or the name of the
|
||||
# organization which holds the copyright to this list in alphabetical order.
|
||||
|
||||
# Names should be added to this file as
|
||||
# Name <email address>
|
||||
# The email address is not required for organizations.
|
||||
# Please keep the list sorted.
|
||||
|
||||
|
||||
# Individual Persons
|
||||
|
||||
Aaron Hopkins <go-sql-driver at die.net>
|
||||
Achille Roussel <achille.roussel at gmail.com>
|
||||
Arne Hormann <arnehormann at gmail.com>
|
||||
Asta Xie <xiemengjun at gmail.com>
|
||||
Bulat Gaifullin <gaifullinbf at gmail.com>
|
||||
Carlos Nieto <jose.carlos at menteslibres.net>
|
||||
Chris Moos <chris at tech9computers.com>
|
||||
Daniel Nichter <nil at codenode.com>
|
||||
Daniël van Eeden <git at myname.nl>
|
||||
Dave Protasowski <dprotaso at gmail.com>
|
||||
DisposaBoy <disposaboy at dby.me>
|
||||
Egor Smolyakov <egorsmkv at gmail.com>
|
||||
Evan Shaw <evan at vendhq.com>
|
||||
Frederick Mayle <frederickmayle at gmail.com>
|
||||
Gustavo Kristic <gkristic at gmail.com>
|
||||
Hanno Braun <mail at hannobraun.com>
|
||||
Henri Yandell <flamefew at gmail.com>
|
||||
Hirotaka Yamamoto <ymmt2005 at gmail.com>
|
||||
ICHINOSE Shogo <shogo82148 at gmail.com>
|
||||
INADA Naoki <songofacandy at gmail.com>
|
||||
Jacek Szwec <szwec.jacek at gmail.com>
|
||||
James Harr <james.harr at gmail.com>
|
||||
Jeff Hodges <jeff at somethingsimilar.com>
|
||||
Jeffrey Charles <jeffreycharles at gmail.com>
|
||||
Jian Zhen <zhenjl at gmail.com>
|
||||
Joshua Prunier <joshua.prunier at gmail.com>
|
||||
Julien Lefevre <julien.lefevr at gmail.com>
|
||||
Julien Schmidt <go-sql-driver at julienschmidt.com>
|
||||
Justin Nuß <nuss.justin at gmail.com>
|
||||
Kamil Dziedzic <kamil at klecza.pl>
|
||||
Kevin Malachowski <kevin at chowski.com>
|
||||
Lennart Rudolph <lrudolph at hmc.edu>
|
||||
Leonardo YongUk Kim <dalinaum at gmail.com>
|
||||
Lion Yang <lion at aosc.xyz>
|
||||
Luca Looz <luca.looz92 at gmail.com>
|
||||
Lucas Liu <extrafliu at gmail.com>
|
||||
Luke Scott <luke at webconnex.com>
|
||||
Maciej Zimnoch <maciej.zimnoch@codilime.com>
|
||||
Michael Woolnough <michael.woolnough at gmail.com>
|
||||
Nicola Peduzzi <thenikso at gmail.com>
|
||||
Olivier Mengué <dolmen at cpan.org>
|
||||
oscarzhao <oscarzhaosl at gmail.com>
|
||||
Paul Bonser <misterpib at gmail.com>
|
||||
Peter Schultz <peter.schultz at classmarkets.com>
|
||||
Rebecca Chin <rchin at pivotal.io>
|
||||
Runrioter Wung <runrioter at gmail.com>
|
||||
Shuode Li <elemount at qq.com>
|
||||
Soroush Pour <me at soroushjp.com>
|
||||
Stan Putrya <root.vagner at gmail.com>
|
||||
Stanley Gunawan <gunawan.stanley at gmail.com>
|
||||
Xiangyu Hu <xiangyu.hu at outlook.com>
|
||||
Xiaobing Jiang <s7v7nislands at gmail.com>
|
||||
Xiuming Chen <cc at cxm.cc>
|
||||
Zhenye Xie <xiezhenye at gmail.com>
|
||||
|
||||
# Organizations
|
||||
|
||||
Barracuda Networks, Inc.
|
||||
Google Inc.
|
||||
Keybase Inc.
|
||||
Pivotal Inc.
|
||||
Stripe Inc.
|
||||
119
vendor/github.com/go-sql-driver/mysql/CHANGELOG.md
generated
vendored
@@ -1,119 +0,0 @@
|
||||
## Version 1.3 (2016-12-01)
|
||||
|
||||
Changes:
|
||||
|
||||
- Go 1.1 is no longer supported
|
||||
- Use decimals fields in MySQL to format time types (#249)
|
||||
- Buffer optimizations (#269)
|
||||
- TLS ServerName defaults to the host (#283)
|
||||
- Refactoring (#400, #410, #437)
|
||||
- Adjusted documentation for second generation CloudSQL (#485)
|
||||
- Documented DSN system var quoting rules (#502)
|
||||
- Made statement.Close() calls idempotent to avoid errors in Go 1.6+ (#512)
|
||||
|
||||
New Features:
|
||||
|
||||
- Enable microsecond resolution on TIME, DATETIME and TIMESTAMP (#249)
|
||||
- Support for returning table alias on Columns() (#289, #359, #382)
|
||||
- Placeholder interpolation, can be actived with the DSN parameter `interpolateParams=true` (#309, #318, #490)
|
||||
- Support for uint64 parameters with high bit set (#332, #345)
|
||||
- Cleartext authentication plugin support (#327)
|
||||
- Exported ParseDSN function and the Config struct (#403, #419, #429)
|
||||
- Read / Write timeouts (#401)
|
||||
- Support for JSON field type (#414)
|
||||
- Support for multi-statements and multi-results (#411, #431)
|
||||
- DSN parameter to set the driver-side max_allowed_packet value manually (#489)
|
||||
- Native password authentication plugin support (#494, #524)
|
||||
|
||||
Bugfixes:
|
||||
|
||||
- Fixed handling of queries without columns and rows (#255)
|
||||
- Fixed a panic when SetKeepAlive() failed (#298)
|
||||
- Handle ERR packets while reading rows (#321)
|
||||
- Fixed reading NULL length-encoded integers in MySQL 5.6+ (#349)
|
||||
- Fixed absolute paths support in LOAD LOCAL DATA INFILE (#356)
|
||||
- Actually zero out bytes in handshake response (#378)
|
||||
- Fixed race condition in registering LOAD DATA INFILE handler (#383)
|
||||
- Fixed tests with MySQL 5.7.9+ (#380)
|
||||
- QueryUnescape TLS config names (#397)
|
||||
- Fixed "broken pipe" error by writing to closed socket (#390)
|
||||
- Fixed LOAD LOCAL DATA INFILE buffering (#424)
|
||||
- Fixed parsing of floats into float64 when placeholders are used (#434)
|
||||
- Fixed DSN tests with Go 1.7+ (#459)
|
||||
- Handle ERR packets while waiting for EOF (#473)
|
||||
- Invalidate connection on error while discarding additional results (#513)
|
||||
- Allow terminating packets of length 0 (#516)
|
||||
|
||||
|
||||
## Version 1.2 (2014-06-03)
|
||||
|
||||
Changes:
|
||||
|
||||
- We switched back to a "rolling release". `go get` installs the current master branch again
|
||||
- Version v1 of the driver will not be maintained anymore. Go 1.0 is no longer supported by this driver
|
||||
- Exported errors to allow easy checking from application code
|
||||
- Enabled TCP Keepalives on TCP connections
|
||||
- Optimized INFILE handling (better buffer size calculation, lazy init, ...)
|
||||
- The DSN parser also checks for a missing separating slash
|
||||
- Faster binary date / datetime to string formatting
|
||||
- Also exported the MySQLWarning type
|
||||
- mysqlConn.Close returns the first error encountered instead of ignoring all errors
|
||||
- writePacket() automatically writes the packet size to the header
|
||||
- readPacket() uses an iterative approach instead of the recursive approach to merge splitted packets
|
||||
|
||||
New Features:
|
||||
|
||||
- `RegisterDial` allows the usage of a custom dial function to establish the network connection
|
||||
- Setting the connection collation is possible with the `collation` DSN parameter. This parameter should be preferred over the `charset` parameter
|
||||
- Logging of critical errors is configurable with `SetLogger`
|
||||
- Google CloudSQL support
|
||||
|
||||
Bugfixes:
|
||||
|
||||
- Allow more than 32 parameters in prepared statements
|
||||
- Various old_password fixes
|
||||
- Fixed TestConcurrent test to pass Go's race detection
|
||||
- Fixed appendLengthEncodedInteger for large numbers
|
||||
- Renamed readLengthEnodedString to readLengthEncodedString and skipLengthEnodedString to skipLengthEncodedString (fixed typo)
|
||||
|
||||
|
||||
## Version 1.1 (2013-11-02)
|
||||
|
||||
Changes:
|
||||
|
||||
- Go-MySQL-Driver now requires Go 1.1
|
||||
- Connections now use the collation `utf8_general_ci` by default. Adding `&charset=UTF8` to the DSN should not be necessary anymore
|
||||
- Made closing rows and connections error tolerant. This allows for example deferring rows.Close() without checking for errors
|
||||
- `[]byte(nil)` is now treated as a NULL value. Before, it was treated like an empty string / `[]byte("")`
|
||||
- DSN parameter values must now be url.QueryEscape'ed. This allows text values to contain special characters, such as '&'.
|
||||
- Use the IO buffer also for writing. This results in zero allocations (by the driver) for most queries
|
||||
- Optimized the buffer for reading
|
||||
- stmt.Query now caches column metadata
|
||||
- New Logo
|
||||
- Changed the copyright header to include all contributors
|
||||
- Improved the LOAD INFILE documentation
|
||||
- The driver struct is now exported to make the driver directly accessible
|
||||
- Refactored the driver tests
|
||||
- Added more benchmarks and moved all to a separate file
|
||||
- Other small refactoring
|
||||
|
||||
New Features:
|
||||
|
||||
- Added *old_passwords* support: Required in some cases, but must be enabled by adding `allowOldPasswords=true` to the DSN since it is insecure
|
||||
- Added a `clientFoundRows` parameter: Return the number of matching rows instead of the number of rows changed on UPDATEs
|
||||
- Added TLS/SSL support: Use a TLS/SSL encrypted connection to the server. Custom TLS configs can be registered and used
|
||||
|
||||
Bugfixes:
|
||||
|
||||
- Fixed MySQL 4.1 support: MySQL 4.1 sends packets with lengths which differ from the specification
|
||||
- Convert to DB timezone when inserting `time.Time`
|
||||
- Splitted packets (more than 16MB) are now merged correctly
|
||||
- Fixed false positive `io.EOF` errors when the data was fully read
|
||||
- Avoid panics on reuse of closed connections
|
||||
- Fixed empty string producing false nil values
|
||||
- Fixed sign byte for positive TIME fields
|
||||
|
||||
|
||||
## Version 1.0 (2013-05-14)
|
||||
|
||||
Initial Release
|
||||
23
vendor/github.com/go-sql-driver/mysql/CONTRIBUTING.md
generated
vendored
@@ -1,23 +0,0 @@
|
||||
# Contributing Guidelines
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Before creating a new Issue, please check first if a similar Issue [already exists](https://github.com/go-sql-driver/mysql/issues?state=open) or was [recently closed](https://github.com/go-sql-driver/mysql/issues?direction=desc&page=1&sort=updated&state=closed).
|
||||
|
||||
## Contributing Code
|
||||
|
||||
By contributing to this project, you share your code under the Mozilla Public License 2, as specified in the LICENSE file.
|
||||
Don't forget to add yourself to the AUTHORS file.
|
||||
|
||||
### Code Review
|
||||
|
||||
Everyone is invited to review and comment on pull requests.
|
||||
If it looks fine to you, comment with "LGTM" (Looks good to me).
|
||||
|
||||
If changes are required, notice the reviewers with "PTAL" (Please take another look) after committing the fixes.
|
||||
|
||||
Before merging the Pull Request, at least one [team member](https://github.com/go-sql-driver?tab=members) must have commented with "LGTM".
|
||||
|
||||
## Development Ideas
|
||||
|
||||
If you are looking for ideas for code contributions, please check our [Development Ideas](https://github.com/go-sql-driver/mysql/wiki/Development-Ideas) Wiki page.
|
||||
373
vendor/github.com/go-sql-driver/mysql/LICENSE
generated
vendored
@@ -1,373 +0,0 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
476
vendor/github.com/go-sql-driver/mysql/README.md
generated
vendored
@@ -1,476 +0,0 @@
|
||||
# Go-MySQL-Driver
|
||||
|
||||
A MySQL-Driver for Go's [database/sql](https://golang.org/pkg/database/sql/) package
|
||||
|
||||

|
||||
|
||||
---------------------------------------
|
||||
* [Features](#features)
|
||||
* [Requirements](#requirements)
|
||||
* [Installation](#installation)
|
||||
* [Usage](#usage)
|
||||
* [DSN (Data Source Name)](#dsn-data-source-name)
|
||||
* [Password](#password)
|
||||
* [Protocol](#protocol)
|
||||
* [Address](#address)
|
||||
* [Parameters](#parameters)
|
||||
* [Examples](#examples)
|
||||
* [Connection pool and timeouts](#connection-pool-and-timeouts)
|
||||
* [context.Context Support](#contextcontext-support)
|
||||
* [ColumnType Support](#columntype-support)
|
||||
* [LOAD DATA LOCAL INFILE support](#load-data-local-infile-support)
|
||||
* [time.Time support](#timetime-support)
|
||||
* [Unicode support](#unicode-support)
|
||||
* [Testing / Development](#testing--development)
|
||||
* [License](#license)
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## Features
|
||||
* Lightweight and [fast](https://github.com/go-sql-driver/sql-benchmark "golang MySQL-Driver performance")
|
||||
* Native Go implementation. No C-bindings, just pure Go
|
||||
* Connections over TCP/IPv4, TCP/IPv6, Unix domain sockets or [custom protocols](https://godoc.org/github.com/go-sql-driver/mysql#DialFunc)
|
||||
* Automatic handling of broken connections
|
||||
* Automatic Connection Pooling *(by database/sql package)*
|
||||
* Supports queries larger than 16MB
|
||||
* Full [`sql.RawBytes`](https://golang.org/pkg/database/sql/#RawBytes) support.
|
||||
* Intelligent `LONG DATA` handling in prepared statements
|
||||
* Secure `LOAD DATA LOCAL INFILE` support with file Whitelisting and `io.Reader` support
|
||||
* Optional `time.Time` parsing
|
||||
* Optional placeholder interpolation
|
||||
|
||||
## Requirements
|
||||
* Go 1.5 or higher
|
||||
* MySQL (4.1+), MariaDB, Percona Server, Google CloudSQL or Sphinx (2.2.3+)
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## Installation
|
||||
Simple install the package to your [$GOPATH](https://github.com/golang/go/wiki/GOPATH "GOPATH") with the [go tool](https://golang.org/cmd/go/ "go command") from shell:
|
||||
```bash
|
||||
$ go get -u github.com/go-sql-driver/mysql
|
||||
```
|
||||
Make sure [Git is installed](https://git-scm.com/downloads) on your machine and in your system's `PATH`.
|
||||
|
||||
## Usage
|
||||
_Go MySQL Driver_ is an implementation of Go's `database/sql/driver` interface. You only need to import the driver and can use the full [`database/sql`](https://golang.org/pkg/database/sql/) API then.
|
||||
|
||||
Use `mysql` as `driverName` and a valid [DSN](#dsn-data-source-name) as `dataSourceName`:
|
||||
```go
|
||||
import "database/sql"
|
||||
import _ "github.com/go-sql-driver/mysql"
|
||||
|
||||
db, err := sql.Open("mysql", "user:password@/dbname")
|
||||
```
|
||||
|
||||
[Examples are available in our Wiki](https://github.com/go-sql-driver/mysql/wiki/Examples "Go-MySQL-Driver Examples").
|
||||
|
||||
|
||||
### DSN (Data Source Name)
|
||||
|
||||
The Data Source Name has a common format, like e.g. [PEAR DB](http://pear.php.net/manual/en/package.database.db.intro-dsn.php) uses it, but without type-prefix (optional parts marked by squared brackets):
|
||||
```
|
||||
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
|
||||
```
|
||||
|
||||
A DSN in its fullest form:
|
||||
```
|
||||
username:password@protocol(address)/dbname?param=value
|
||||
```
|
||||
|
||||
Except for the databasename, all values are optional. So the minimal DSN is:
|
||||
```
|
||||
/dbname
|
||||
```
|
||||
|
||||
If you do not want to preselect a database, leave `dbname` empty:
|
||||
```
|
||||
/
|
||||
```
|
||||
This has the same effect as an empty DSN string:
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
Alternatively, [Config.FormatDSN](https://godoc.org/github.com/go-sql-driver/mysql#Config.FormatDSN) can be used to create a DSN string by filling a struct.
|
||||
|
||||
#### Password
|
||||
Passwords can consist of any character. Escaping is **not** necessary.
|
||||
|
||||
#### Protocol
|
||||
See [net.Dial](https://golang.org/pkg/net/#Dial) for more information which networks are available.
|
||||
In general you should use an Unix domain socket if available and TCP otherwise for best performance.
|
||||
|
||||
#### Address
|
||||
For TCP and UDP networks, addresses have the form `host[:port]`.
|
||||
If `port` is omitted, the default port will be used.
|
||||
If `host` is a literal IPv6 address, it must be enclosed in square brackets.
|
||||
The functions [net.JoinHostPort](https://golang.org/pkg/net/#JoinHostPort) and [net.SplitHostPort](https://golang.org/pkg/net/#SplitHostPort) manipulate addresses in this form.
|
||||
|
||||
For Unix domain sockets the address is the absolute path to the MySQL-Server-socket, e.g. `/var/run/mysqld/mysqld.sock` or `/tmp/mysql.sock`.
|
||||
|
||||
#### Parameters
|
||||
*Parameters are case-sensitive!*
|
||||
|
||||
Notice that any of `true`, `TRUE`, `True` or `1` is accepted to stand for a true boolean value. Not surprisingly, false can be specified as any of: `false`, `FALSE`, `False` or `0`.
|
||||
|
||||
##### `allowAllFiles`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
`allowAllFiles=true` disables the file Whitelist for `LOAD DATA LOCAL INFILE` and allows *all* files.
|
||||
[*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)
|
||||
|
||||
##### `allowCleartextPasswords`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
`allowCleartextPasswords=true` allows using the [cleartext client side plugin](http://dev.mysql.com/doc/en/cleartext-authentication-plugin.html) if required by an account, such as one defined with the [PAM authentication plugin](http://dev.mysql.com/doc/en/pam-authentication-plugin.html). Sending passwords in clear text may be a security problem in some configurations. To avoid problems if there is any possibility that the password would be intercepted, clients should connect to MySQL Server using a method that protects the password. Possibilities include [TLS / SSL](#tls), IPsec, or a private network.
|
||||
|
||||
##### `allowNativePasswords`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: true
|
||||
```
|
||||
`allowNativePasswords=false` disallows the usage of MySQL native password method.
|
||||
|
||||
##### `allowOldPasswords`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
`allowOldPasswords=true` allows the usage of the insecure old password method. This should be avoided, but is necessary in some cases. See also [the old_passwords wiki page](https://github.com/go-sql-driver/mysql/wiki/old_passwords).
|
||||
|
||||
##### `charset`
|
||||
|
||||
```
|
||||
Type: string
|
||||
Valid Values: <name>
|
||||
Default: none
|
||||
```
|
||||
|
||||
Sets the charset used for client-server interaction (`"SET NAMES <value>"`). If multiple charsets are set (separated by a comma), the following charset is used if setting the charset failes. This enables for example support for `utf8mb4` ([introduced in MySQL 5.5.3](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html)) with fallback to `utf8` for older servers (`charset=utf8mb4,utf8`).
|
||||
|
||||
Usage of the `charset` parameter is discouraged because it issues additional queries to the server.
|
||||
Unless you need the fallback behavior, please use `collation` instead.
|
||||
|
||||
##### `collation`
|
||||
|
||||
```
|
||||
Type: string
|
||||
Valid Values: <name>
|
||||
Default: utf8_general_ci
|
||||
```
|
||||
|
||||
Sets the collation used for client-server interaction on connection. In contrast to `charset`, `collation` does not issue additional queries. If the specified collation is unavailable on the target server, the connection will fail.
|
||||
|
||||
A list of valid charsets for a server is retrievable with `SHOW COLLATION`.
|
||||
|
||||
##### `clientFoundRows`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
`clientFoundRows=true` causes an UPDATE to return the number of matching rows instead of the number of rows changed.
|
||||
|
||||
##### `columnsWithAlias`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
When `columnsWithAlias` is true, calls to `sql.Rows.Columns()` will return the table alias and the column name separated by a dot. For example:
|
||||
|
||||
```
|
||||
SELECT u.id FROM users as u
|
||||
```
|
||||
|
||||
will return `u.id` instead of just `id` if `columnsWithAlias=true`.
|
||||
|
||||
##### `interpolateParams`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
If `interpolateParams` is true, placeholders (`?`) in calls to `db.Query()` and `db.Exec()` are interpolated into a single query string with given parameters. This reduces the number of roundtrips, since the driver has to prepare a statement, execute it with given parameters and close the statement again with `interpolateParams=false`.
|
||||
|
||||
*This can not be used together with the multibyte encodings BIG5, CP932, GB2312, GBK or SJIS. These are blacklisted as they may [introduce a SQL injection vulnerability](http://stackoverflow.com/a/12118602/3430118)!*
|
||||
|
||||
##### `loc`
|
||||
|
||||
```
|
||||
Type: string
|
||||
Valid Values: <escaped name>
|
||||
Default: UTC
|
||||
```
|
||||
|
||||
Sets the location for time.Time values (when using `parseTime=true`). *"Local"* sets the system's location. See [time.LoadLocation](https://golang.org/pkg/time/#LoadLocation) for details.
|
||||
|
||||
Note that this sets the location for time.Time values but does not change MySQL's [time_zone setting](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html). For that see the [time_zone system variable](#system-variables), which can also be set as a DSN parameter.
|
||||
|
||||
Please keep in mind, that param values must be [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)'ed. Alternatively you can manually replace the `/` with `%2F`. For example `US/Pacific` would be `loc=US%2FPacific`.
|
||||
|
||||
##### `maxAllowedPacket`
|
||||
```
|
||||
Type: decimal number
|
||||
Default: 4194304
|
||||
```
|
||||
|
||||
Max packet size allowed in bytes. The default value is 4 MiB and should be adjusted to match the server settings. `maxAllowedPacket=0` can be used to automatically fetch the `max_allowed_packet` variable from server *on every connection*.
|
||||
|
||||
##### `multiStatements`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
Allow multiple statements in one query. While this allows batch queries, it also greatly increases the risk of SQL injections. Only the result of the first query is returned, all other results are silently discarded.
|
||||
|
||||
When `multiStatements` is used, `?` parameters must only be used in the first statement.
|
||||
|
||||
##### `parseTime`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
`parseTime=true` changes the output type of `DATE` and `DATETIME` values to `time.Time` instead of `[]byte` / `string`
|
||||
|
||||
|
||||
##### `readTimeout`
|
||||
|
||||
```
|
||||
Type: duration
|
||||
Default: 0
|
||||
```
|
||||
|
||||
I/O read timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
|
||||
|
||||
##### `rejectReadOnly`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
|
||||
`rejectReadOnly=true` causes the driver to reject read-only connections. This
|
||||
is for a possible race condition during an automatic failover, where the mysql
|
||||
client gets connected to a read-only replica after the failover.
|
||||
|
||||
Note that this should be a fairly rare case, as an automatic failover normally
|
||||
happens when the primary is down, and the race condition shouldn't happen
|
||||
unless it comes back up online as soon as the failover is kicked off. On the
|
||||
other hand, when this happens, a MySQL application can get stuck on a
|
||||
read-only connection until restarted. It is however fairly easy to reproduce,
|
||||
for example, using a manual failover on AWS Aurora's MySQL-compatible cluster.
|
||||
|
||||
If you are not relying on read-only transactions to reject writes that aren't
|
||||
supposed to happen, setting this on some MySQL providers (such as AWS Aurora)
|
||||
is safer for failovers.
|
||||
|
||||
Note that ERROR 1290 can be returned for a `read-only` server and this option will
|
||||
cause a retry for that error. However the same error number is used for some
|
||||
other cases. You should ensure your application will never cause an ERROR 1290
|
||||
except for `read-only` mode when enabling this option.
|
||||
|
||||
|
||||
##### `timeout`
|
||||
|
||||
```
|
||||
Type: duration
|
||||
Default: OS default
|
||||
```
|
||||
|
||||
Timeout for establishing connections, aka dial timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
|
||||
|
||||
|
||||
##### `tls`
|
||||
|
||||
```
|
||||
Type: bool / string
|
||||
Valid Values: true, false, skip-verify, <name>
|
||||
Default: false
|
||||
```
|
||||
|
||||
`tls=true` enables TLS / SSL encrypted connection to the server. Use `skip-verify` if you want to use a self-signed or invalid certificate (server side). Use a custom value registered with [`mysql.RegisterTLSConfig`](https://godoc.org/github.com/go-sql-driver/mysql#RegisterTLSConfig).
|
||||
|
||||
|
||||
##### `writeTimeout`
|
||||
|
||||
```
|
||||
Type: duration
|
||||
Default: 0
|
||||
```
|
||||
|
||||
I/O write timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
|
||||
|
||||
|
||||
##### System Variables
|
||||
|
||||
Any other parameters are interpreted as system variables:
|
||||
* `<boolean_var>=<value>`: `SET <boolean_var>=<value>`
|
||||
* `<enum_var>=<value>`: `SET <enum_var>=<value>`
|
||||
* `<string_var>=%27<value>%27`: `SET <string_var>='<value>'`
|
||||
|
||||
Rules:
|
||||
* The values for string variables must be quoted with `'`.
|
||||
* The values must also be [url.QueryEscape](http://golang.org/pkg/net/url/#QueryEscape)'ed!
|
||||
(which implies values of string variables must be wrapped with `%27`).
|
||||
|
||||
Examples:
|
||||
* `autocommit=1`: `SET autocommit=1`
|
||||
* [`time_zone=%27Europe%2FParis%27`](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html): `SET time_zone='Europe/Paris'`
|
||||
* [`tx_isolation=%27REPEATABLE-READ%27`](https://dev.mysql.com/doc/refman/5.5/en/server-system-variables.html#sysvar_tx_isolation): `SET tx_isolation='REPEATABLE-READ'`
|
||||
|
||||
|
||||
#### Examples
|
||||
```
|
||||
user@unix(/path/to/socket)/dbname
|
||||
```
|
||||
|
||||
```
|
||||
root:pw@unix(/tmp/mysql.sock)/myDatabase?loc=Local
|
||||
```
|
||||
|
||||
```
|
||||
user:password@tcp(localhost:5555)/dbname?tls=skip-verify&autocommit=true
|
||||
```
|
||||
|
||||
Treat warnings as errors by setting the system variable [`sql_mode`](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html):
|
||||
```
|
||||
user:password@/dbname?sql_mode=TRADITIONAL
|
||||
```
|
||||
|
||||
TCP via IPv6:
|
||||
```
|
||||
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname?timeout=90s&collation=utf8mb4_unicode_ci
|
||||
```
|
||||
|
||||
TCP on a remote host, e.g. Amazon RDS:
|
||||
```
|
||||
id:password@tcp(your-amazonaws-uri.com:3306)/dbname
|
||||
```
|
||||
|
||||
Google Cloud SQL on App Engine (First Generation MySQL Server):
|
||||
```
|
||||
user@cloudsql(project-id:instance-name)/dbname
|
||||
```
|
||||
|
||||
Google Cloud SQL on App Engine (Second Generation MySQL Server):
|
||||
```
|
||||
user@cloudsql(project-id:regionname:instance-name)/dbname
|
||||
```
|
||||
|
||||
TCP using default port (3306) on localhost:
|
||||
```
|
||||
user:password@tcp/dbname?charset=utf8mb4,utf8&sys_var=esc%40ped
|
||||
```
|
||||
|
||||
Use the default protocol (tcp) and host (localhost:3306):
|
||||
```
|
||||
user:password@/dbname
|
||||
```
|
||||
|
||||
No Database preselected:
|
||||
```
|
||||
user:password@/
|
||||
```
|
||||
|
||||
|
||||
### Connection pool and timeouts
|
||||
The connection pool is managed by Go's database/sql package. For details on how to configure the size of the pool and how long connections stay in the pool see `*DB.SetMaxOpenConns`, `*DB.SetMaxIdleConns`, and `*DB.SetConnMaxLifetime` in the [database/sql documentation](https://golang.org/pkg/database/sql/). The read, write, and dial timeouts for each individual connection are configured with the DSN parameters [`readTimeout`](#readtimeout), [`writeTimeout`](#writetimeout), and [`timeout`](#timeout), respectively.
|
||||
|
||||
## `ColumnType` Support
|
||||
This driver supports the [`ColumnType` interface](https://golang.org/pkg/database/sql/#ColumnType) introduced in Go 1.8, with the exception of [`ColumnType.Length()`](https://golang.org/pkg/database/sql/#ColumnType.Length), which is currently not supported.
|
||||
|
||||
## `context.Context` Support
|
||||
Go 1.8 added `database/sql` support for `context.Context`. This driver supports query timeouts and cancellation via contexts.
|
||||
See [context support in the database/sql package](https://golang.org/doc/go1.8#database_sql) for more details.
|
||||
|
||||
|
||||
### `LOAD DATA LOCAL INFILE` support
|
||||
For this feature you need direct access to the package. Therefore you must change the import path (no `_`):
|
||||
```go
|
||||
import "github.com/go-sql-driver/mysql"
|
||||
```
|
||||
|
||||
Files must be whitelisted by registering them with `mysql.RegisterLocalFile(filepath)` (recommended) or the Whitelist check must be deactivated by using the DSN parameter `allowAllFiles=true` ([*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)).
|
||||
|
||||
To use a `io.Reader` a handler function must be registered with `mysql.RegisterReaderHandler(name, handler)` which returns a `io.Reader` or `io.ReadCloser`. The Reader is available with the filepath `Reader::<name>` then. Choose different names for different handlers and `DeregisterReaderHandler` when you don't need it anymore.
|
||||
|
||||
See the [godoc of Go-MySQL-Driver](https://godoc.org/github.com/go-sql-driver/mysql "golang mysql driver documentation") for details.
|
||||
|
||||
|
||||
### `time.Time` support
|
||||
The default internal output type of MySQL `DATE` and `DATETIME` values is `[]byte` which allows you to scan the value into a `[]byte`, `string` or `sql.RawBytes` variable in your program.
|
||||
|
||||
However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` variables, which is the logical opposite in Go to `DATE` and `DATETIME` in MySQL. You can do that by changing the internal output type from `[]byte` to `time.Time` with the DSN parameter `parseTime=true`. You can set the default [`time.Time` location](https://golang.org/pkg/time/#Location) with the `loc` DSN parameter.
|
||||
|
||||
**Caution:** As of Go 1.1, this makes `time.Time` the only variable type you can scan `DATE` and `DATETIME` values into. This breaks for example [`sql.RawBytes` support](https://github.com/go-sql-driver/mysql/wiki/Examples#rawbytes).
|
||||
|
||||
Alternatively you can use the [`NullTime`](https://godoc.org/github.com/go-sql-driver/mysql#NullTime) type as the scan destination, which works with both `time.Time` and `string` / `[]byte`.
|
||||
|
||||
|
||||
### Unicode support
|
||||
Since version 1.1 Go-MySQL-Driver automatically uses the collation `utf8_general_ci` by default.
|
||||
|
||||
Other collations / charsets can be set using the [`collation`](#collation) DSN parameter.
|
||||
|
||||
Version 1.0 of the driver recommended adding `&charset=utf8` (alias for `SET NAMES utf8`) to the DSN to enable proper UTF-8 support. This is not necessary anymore. The [`collation`](#collation) parameter should be preferred to set another collation / charset than the default.
|
||||
|
||||
See http://dev.mysql.com/doc/refman/5.7/en/charset-unicode.html for more details on MySQL's Unicode support.
|
||||
|
||||
## Testing / Development
|
||||
To run the driver tests you may need to adjust the configuration. See the [Testing Wiki-Page](https://github.com/go-sql-driver/mysql/wiki/Testing "Testing") for details.
|
||||
|
||||
Go-MySQL-Driver is not feature-complete yet. Your help is very appreciated.
|
||||
If you want to contribute, you can work on an [open issue](https://github.com/go-sql-driver/mysql/issues?state=open) or review a [pull request](https://github.com/go-sql-driver/mysql/pulls).
|
||||
|
||||
See the [Contribution Guidelines](https://github.com/go-sql-driver/mysql/blob/master/CONTRIBUTING.md) for details.
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## License
|
||||
Go-MySQL-Driver is licensed under the [Mozilla Public License Version 2.0](https://raw.github.com/go-sql-driver/mysql/master/LICENSE)
|
||||
|
||||
Mozilla summarizes the license scope as follows:
|
||||
> MPL: The copyleft applies to any files containing MPLed code.
|
||||
|
||||
|
||||
That means:
|
||||
* You can **use** the **unchanged** source code both in private and commercially.
|
||||
* When distributing, you **must publish** the source code of any **changed files** licensed under the MPL 2.0 under a) the MPL 2.0 itself or b) a compatible license (e.g. GPL 3.0 or Apache License 2.0).
|
||||
* You **needn't publish** the source code of your library as long as the files licensed under the MPL 2.0 are **unchanged**.
|
||||
|
||||
Please read the [MPL 2.0 FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/) if you have further questions regarding the license.
|
||||
|
||||
You can read the full terms here: [LICENSE](https://raw.github.com/go-sql-driver/mysql/master/LICENSE).
|
||||
|
||||

|
||||
|
||||
19
vendor/github.com/go-sql-driver/mysql/appengine.go
generated
vendored
@@ -1,19 +0,0 @@
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build appengine
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"appengine/cloudsql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterDial("cloudsql", cloudsql.Dial)
|
||||
}
|
||||
147
vendor/github.com/go-sql-driver/mysql/buffer.go
generated
vendored
@@ -1,147 +0,0 @@
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultBufSize = 4096
|
||||
|
||||
// A buffer which is used for both reading and writing.
|
||||
// This is possible since communication on each connection is synchronous.
|
||||
// In other words, we can't write and read simultaneously on the same connection.
|
||||
// The buffer is similar to bufio.Reader / Writer but zero-copy-ish
|
||||
// Also highly optimized for this particular use case.
|
||||
type buffer struct {
|
||||
buf []byte
|
||||
nc net.Conn
|
||||
idx int
|
||||
length int
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func newBuffer(nc net.Conn) buffer {
|
||||
var b [defaultBufSize]byte
|
||||
return buffer{
|
||||
buf: b[:],
|
||||
nc: nc,
|
||||
}
|
||||
}
|
||||
|
||||
// fill reads into the buffer until at least _need_ bytes are in it
|
||||
func (b *buffer) fill(need int) error {
|
||||
n := b.length
|
||||
|
||||
// move existing data to the beginning
|
||||
if n > 0 && b.idx > 0 {
|
||||
copy(b.buf[0:n], b.buf[b.idx:])
|
||||
}
|
||||
|
||||
// grow buffer if necessary
|
||||
// TODO: let the buffer shrink again at some point
|
||||
// Maybe keep the org buf slice and swap back?
|
||||
if need > len(b.buf) {
|
||||
// Round up to the next multiple of the default size
|
||||
newBuf := make([]byte, ((need/defaultBufSize)+1)*defaultBufSize)
|
||||
copy(newBuf, b.buf)
|
||||
b.buf = newBuf
|
||||
}
|
||||
|
||||
b.idx = 0
|
||||
|
||||
for {
|
||||
if b.timeout > 0 {
|
||||
if err := b.nc.SetReadDeadline(time.Now().Add(b.timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
nn, err := b.nc.Read(b.buf[n:])
|
||||
n += nn
|
||||
|
||||
switch err {
|
||||
case nil:
|
||||
if n < need {
|
||||
continue
|
||||
}
|
||||
b.length = n
|
||||
return nil
|
||||
|
||||
case io.EOF:
|
||||
if n >= need {
|
||||
b.length = n
|
||||
return nil
|
||||
}
|
||||
return io.ErrUnexpectedEOF
|
||||
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns next N bytes from buffer.
|
||||
// The returned slice is only guaranteed to be valid until the next read
|
||||
func (b *buffer) readNext(need int) ([]byte, error) {
|
||||
if b.length < need {
|
||||
// refill
|
||||
if err := b.fill(need); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
offset := b.idx
|
||||
b.idx += need
|
||||
b.length -= need
|
||||
return b.buf[offset:b.idx], nil
|
||||
}
|
||||
|
||||
// returns a buffer with the requested size.
|
||||
// If possible, a slice from the existing buffer is returned.
|
||||
// Otherwise a bigger buffer is made.
|
||||
// Only one buffer (total) can be used at a time.
|
||||
func (b *buffer) takeBuffer(length int) []byte {
|
||||
if b.length > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// test (cheap) general case first
|
||||
if length <= defaultBufSize || length <= cap(b.buf) {
|
||||
return b.buf[:length]
|
||||
}
|
||||
|
||||
if length < maxPacketSize {
|
||||
b.buf = make([]byte, length)
|
||||
return b.buf
|
||||
}
|
||||
return make([]byte, length)
|
||||
}
|
||||
|
||||
// shortcut which can be used if the requested buffer is guaranteed to be
|
||||
// smaller than defaultBufSize
|
||||
// Only one buffer (total) can be used at a time.
|
||||
func (b *buffer) takeSmallBuffer(length int) []byte {
|
||||
if b.length == 0 {
|
||||
return b.buf[:length]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// takeCompleteBuffer returns the complete existing buffer.
|
||||
// This can be used if the necessary buffer size is unknown.
|
||||
// Only one buffer (total) can be used at a time.
|
||||
func (b *buffer) takeCompleteBuffer() []byte {
|
||||
if b.length == 0 {
|
||||
return b.buf
|
||||
}
|
||||
return nil
|
||||
}
|
||||
250
vendor/github.com/go-sql-driver/mysql/collations.go
generated
vendored
@@ -1,250 +0,0 @@
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2014 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package mysql
|
||||
|
||||
const defaultCollation = "utf8_general_ci"
|
||||
|
||||
// A list of available collations mapped to the internal ID.
|
||||
// To update this map use the following MySQL query:
|
||||
// SELECT COLLATION_NAME, ID FROM information_schema.COLLATIONS
|
||||
var collations = map[string]byte{
|
||||
"big5_chinese_ci": 1,
|
||||
"latin2_czech_cs": 2,
|
||||
"dec8_swedish_ci": 3,
|
||||
"cp850_general_ci": 4,
|
||||
"latin1_german1_ci": 5,
|
||||
"hp8_english_ci": 6,
|
||||
"koi8r_general_ci": 7,
|
||||
"latin1_swedish_ci": 8,
|
||||
"latin2_general_ci": 9,
|
||||
"swe7_swedish_ci": 10,
|
||||
"ascii_general_ci": 11,
|
||||
"ujis_japanese_ci": 12,
|
||||
"sjis_japanese_ci": 13,
|
||||
"cp1251_bulgarian_ci": 14,
|
||||
"latin1_danish_ci": 15,
|
||||
"hebrew_general_ci": 16,
|
||||
"tis620_thai_ci": 18,
|
||||
"euckr_korean_ci": 19,
|
||||
"latin7_estonian_cs": 20,
|
||||
"latin2_hungarian_ci": 21,
|
||||
"koi8u_general_ci": 22,
|
||||
"cp1251_ukrainian_ci": 23,
|
||||
"gb2312_chinese_ci": 24,
|
||||
"greek_general_ci": 25,
|
||||
"cp1250_general_ci": 26,
|
||||
"latin2_croatian_ci": 27,
|
||||
"gbk_chinese_ci": 28,
|
||||
"cp1257_lithuanian_ci": 29,
|
||||
"latin5_turkish_ci": 30,
|
||||
"latin1_german2_ci": 31,
|
||||
"armscii8_general_ci": 32,
|
||||
"utf8_general_ci": 33,
|
||||
"cp1250_czech_cs": 34,
|
||||
"ucs2_general_ci": 35,
|
||||
"cp866_general_ci": 36,
|
||||
"keybcs2_general_ci": 37,
|
||||
"macce_general_ci": 38,
|
||||
"macroman_general_ci": 39,
|
||||
"cp852_general_ci": 40,
|
||||
"latin7_general_ci": 41,
|
||||
"latin7_general_cs": 42,
|
||||
"macce_bin": 43,
|
||||
"cp1250_croatian_ci": 44,
|
||||
"utf8mb4_general_ci": 45,
|
||||
"utf8mb4_bin": 46,
|
||||
"latin1_bin": 47,
|
||||
"latin1_general_ci": 48,
|
||||
"latin1_general_cs": 49,
|
||||
"cp1251_bin": 50,
|
||||
"cp1251_general_ci": 51,
|
||||
"cp1251_general_cs": 52,
|
||||
"macroman_bin": 53,
|
||||
"utf16_general_ci": 54,
|
||||
"utf16_bin": 55,
|
||||
"utf16le_general_ci": 56,
|
||||
"cp1256_general_ci": 57,
|
||||
"cp1257_bin": 58,
|
||||
"cp1257_general_ci": 59,
|
||||
"utf32_general_ci": 60,
|
||||
"utf32_bin": 61,
|
||||
"utf16le_bin": 62,
|
||||
"binary": 63,
|
||||
"armscii8_bin": 64,
|
||||
"ascii_bin": 65,
|
||||
"cp1250_bin": 66,
|
||||
"cp1256_bin": 67,
|
||||
"cp866_bin": 68,
|
||||
"dec8_bin": 69,
|
||||
"greek_bin": 70,
|
||||
"hebrew_bin": 71,
|
||||
"hp8_bin": 72,
|
||||
"keybcs2_bin": 73,
|
||||
"koi8r_bin": 74,
|
||||
"koi8u_bin": 75,
|
||||
"latin2_bin": 77,
|
||||
"latin5_bin": 78,
|
||||
"latin7_bin": 79,
|
||||
"cp850_bin": 80,
|
||||
"cp852_bin": 81,
|
||||
"swe7_bin": 82,
|
||||
"utf8_bin": 83,
|
||||
"big5_bin": 84,
|
||||
"euckr_bin": 85,
|
||||
"gb2312_bin": 86,
|
||||
"gbk_bin": 87,
|
||||
"sjis_bin": 88,
|
||||
"tis620_bin": 89,
|
||||
"ucs2_bin": 90,
|
||||
"ujis_bin": 91,
|
||||
"geostd8_general_ci": 92,
|
||||
"geostd8_bin": 93,
|
||||
"latin1_spanish_ci": 94,
|
||||
"cp932_japanese_ci": 95,
|
||||
"cp932_bin": 96,
|
||||
"eucjpms_japanese_ci": 97,
|
||||
"eucjpms_bin": 98,
|
||||
"cp1250_polish_ci": 99,
|
||||
"utf16_unicode_ci": 101,
|
||||
"utf16_icelandic_ci": 102,
|
||||
"utf16_latvian_ci": 103,
|
||||
"utf16_romanian_ci": 104,
|
||||
"utf16_slovenian_ci": 105,
|
||||
"utf16_polish_ci": 106,
|
||||
"utf16_estonian_ci": 107,
|
||||
"utf16_spanish_ci": 108,
|
||||
"utf16_swedish_ci": 109,
|
||||
"utf16_turkish_ci": 110,
|
||||
"utf16_czech_ci": 111,
|
||||
"utf16_danish_ci": 112,
|
||||
"utf16_lithuanian_ci": 113,
|
||||
"utf16_slovak_ci": 114,
|
||||
"utf16_spanish2_ci": 115,
|
||||
"utf16_roman_ci": 116,
|
||||
"utf16_persian_ci": 117,
|
||||
"utf16_esperanto_ci": 118,
|
||||
"utf16_hungarian_ci": 119,
|
||||
"utf16_sinhala_ci": 120,
|
||||
"utf16_german2_ci": 121,
|
||||
"utf16_croatian_ci": 122,
|
||||
"utf16_unicode_520_ci": 123,
|
||||
"utf16_vietnamese_ci": 124,
|
||||
"ucs2_unicode_ci": 128,
|
||||
"ucs2_icelandic_ci": 129,
|
||||
"ucs2_latvian_ci": 130,
|
||||
"ucs2_romanian_ci": 131,
|
||||
"ucs2_slovenian_ci": 132,
|
||||
"ucs2_polish_ci": 133,
|
||||
"ucs2_estonian_ci": 134,
|
||||
"ucs2_spanish_ci": 135,
|
||||
"ucs2_swedish_ci": 136,
|
||||
"ucs2_turkish_ci": 137,
|
||||
"ucs2_czech_ci": 138,
|
||||
"ucs2_danish_ci": 139,
|
||||
"ucs2_lithuanian_ci": 140,
|
||||
"ucs2_slovak_ci": 141,
|
||||
"ucs2_spanish2_ci": 142,
|
||||
"ucs2_roman_ci": 143,
|
||||
"ucs2_persian_ci": 144,
|
||||
"ucs2_esperanto_ci": 145,
|
||||
"ucs2_hungarian_ci": 146,
|
||||
"ucs2_sinhala_ci": 147,
|
||||
"ucs2_german2_ci": 148,
|
||||
"ucs2_croatian_ci": 149,
|
||||
"ucs2_unicode_520_ci": 150,
|
||||
"ucs2_vietnamese_ci": 151,
|
||||
"ucs2_general_mysql500_ci": 159,
|
||||
"utf32_unicode_ci": 160,
|
||||
"utf32_icelandic_ci": 161,
|
||||
"utf32_latvian_ci": 162,
|
||||
"utf32_romanian_ci": 163,
|
||||
"utf32_slovenian_ci": 164,
|
||||
"utf32_polish_ci": 165,
|
||||
"utf32_estonian_ci": 166,
|
||||
"utf32_spanish_ci": 167,
|
||||
"utf32_swedish_ci": 168,
|
||||
"utf32_turkish_ci": 169,
|
||||
"utf32_czech_ci": 170,
|
||||
"utf32_danish_ci": 171,
|
||||
"utf32_lithuanian_ci": 172,
|
||||
"utf32_slovak_ci": 173,
|
||||
"utf32_spanish2_ci": 174,
|
||||
"utf32_roman_ci": 175,
|
||||
"utf32_persian_ci": 176,
|
||||
"utf32_esperanto_ci": 177,
|
||||
"utf32_hungarian_ci": 178,
|
||||
"utf32_sinhala_ci": 179,
|
||||
"utf32_german2_ci": 180,
|
||||
"utf32_croatian_ci": 181,
|
||||
"utf32_unicode_520_ci": 182,
|
||||
"utf32_vietnamese_ci": 183,
|
||||
"utf8_unicode_ci": 192,
|
||||
"utf8_icelandic_ci": 193,
|
||||
"utf8_latvian_ci": 194,
|
||||
"utf8_romanian_ci": 195,
|
||||
"utf8_slovenian_ci": 196,
|
||||
"utf8_polish_ci": 197,
|
||||
"utf8_estonian_ci": 198,
|
||||
"utf8_spanish_ci": 199,
|
||||
"utf8_swedish_ci": 200,
|
||||
"utf8_turkish_ci": 201,
|
||||
"utf8_czech_ci": 202,
|
||||
"utf8_danish_ci": 203,
|
||||
"utf8_lithuanian_ci": 204,
|
||||
"utf8_slovak_ci": 205,
|
||||
"utf8_spanish2_ci": 206,
|
||||
"utf8_roman_ci": 207,
|
||||
"utf8_persian_ci": 208,
|
||||
"utf8_esperanto_ci": 209,
|
||||
"utf8_hungarian_ci": 210,
|
||||
"utf8_sinhala_ci": 211,
|
||||
"utf8_german2_ci": 212,
|
||||
"utf8_croatian_ci": 213,
|
||||
"utf8_unicode_520_ci": 214,
|
||||
"utf8_vietnamese_ci": 215,
|
||||
"utf8_general_mysql500_ci": 223,
|
||||
"utf8mb4_unicode_ci": 224,
|
||||
"utf8mb4_icelandic_ci": 225,
|
||||
"utf8mb4_latvian_ci": 226,
|
||||
"utf8mb4_romanian_ci": 227,
|
||||
"utf8mb4_slovenian_ci": 228,
|
||||
"utf8mb4_polish_ci": 229,
|
||||
"utf8mb4_estonian_ci": 230,
|
||||
"utf8mb4_spanish_ci": 231,
|
||||
"utf8mb4_swedish_ci": 232,
|
||||
"utf8mb4_turkish_ci": 233,
|
||||
"utf8mb4_czech_ci": 234,
|
||||
"utf8mb4_danish_ci": 235,
|
||||
"utf8mb4_lithuanian_ci": 236,
|
||||
"utf8mb4_slovak_ci": 237,
|
||||
"utf8mb4_spanish2_ci": 238,
|
||||
"utf8mb4_roman_ci": 239,
|
||||
"utf8mb4_persian_ci": 240,
|
||||
"utf8mb4_esperanto_ci": 241,
|
||||
"utf8mb4_hungarian_ci": 242,
|
||||
"utf8mb4_sinhala_ci": 243,
|
||||
"utf8mb4_german2_ci": 244,
|
||||
"utf8mb4_croatian_ci": 245,
|
||||
"utf8mb4_unicode_520_ci": 246,
|
||||
"utf8mb4_vietnamese_ci": 247,
|
||||
}
|
||||
|
||||
// A blacklist of collations which is unsafe to interpolate parameters.
|
||||
// These multibyte encodings may contains 0x5c (`\`) in their trailing bytes.
|
||||
var unsafeCollations = map[string]bool{
|
||||
"big5_chinese_ci": true,
|
||||
"sjis_japanese_ci": true,
|
||||
"gbk_chinese_ci": true,
|
||||
"big5_bin": true,
|
||||
"gb2312_bin": true,
|
||||
"gbk_bin": true,
|
||||
"sjis_bin": true,
|
||||
"cp932_japanese_ci": true,
|
||||
"cp932_bin": true,
|
||||
}
|
||||
461
vendor/github.com/go-sql-driver/mysql/connection.go
generated
vendored
@@ -1,461 +0,0 @@
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// a copy of context.Context for Go 1.7 and earlier
|
||||
type mysqlContext interface {
|
||||
Done() <-chan struct{}
|
||||
Err() error
|
||||
|
||||
// defined in context.Context, but not used in this driver:
|
||||
// Deadline() (deadline time.Time, ok bool)
|
||||
// Value(key interface{}) interface{}
|
||||
}
|
||||
|
||||
type mysqlConn struct {
|
||||
buf buffer
|
||||
netConn net.Conn
|
||||
affectedRows uint64
|
||||
insertId uint64
|
||||
cfg *Config
|
||||
maxAllowedPacket int
|
||||
maxWriteSize int
|
||||
writeTimeout time.Duration
|
||||
flags clientFlag
|
||||
status statusFlag
|
||||
sequence uint8
|
||||
parseTime bool
|
||||
|
||||
// for context support (Go 1.8+)
|
||||
watching bool
|
||||
watcher chan<- mysqlContext
|
||||
closech chan struct{}
|
||||
finished chan<- struct{}
|
||||
canceled atomicError // set non-nil if conn is canceled
|
||||
closed atomicBool // set when conn is closed, before closech is closed
|
||||
}
|
||||
|
||||
// Handles parameters set in DSN after the connection is established
|
||||
func (mc *mysqlConn) handleParams() (err error) {
|
||||
for param, val := range mc.cfg.Params {
|
||||
switch param {
|
||||
// Charset
|
||||
case "charset":
|
||||
charsets := strings.Split(val, ",")
|
||||
for i := range charsets {
|
||||
// ignore errors here - a charset may not exist
|
||||
err = mc.exec("SET NAMES " + charsets[i])
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// System Vars
|
||||
default:
|
||||
err = mc.exec("SET " + param + "=" + val + "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) markBadConn(err error) error {
|
||||
if mc == nil {
|
||||
return err
|
||||
}
|
||||
if err != errBadConnNoWrite {
|
||||
return err
|
||||
}
|
||||
return driver.ErrBadConn
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) Begin() (driver.Tx, error) {
|
||||
return mc.begin(false)
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) begin(readOnly bool) (driver.Tx, error) {
|
||||
if mc.closed.IsSet() {
|
||||
errLog.Print(ErrInvalidConn)
|
||||
return nil, driver.ErrBadConn
|
||||
}
|
||||
var q string
|
||||
if readOnly {
|
||||
q = "START TRANSACTION READ ONLY"
|
||||
} else {
|
||||
q = "START TRANSACTION"
|
||||
}
|
||||
err := mc.exec(q)
|
||||
if err == nil {
|
||||
return &mysqlTx{mc}, err
|
||||
}
|
||||
return nil, mc.markBadConn(err)
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) Close() (err error) {
|
||||
// Makes Close idempotent
|
||||
if !mc.closed.IsSet() {
|
||||
err = mc.writeCommandPacket(comQuit)
|
||||
}
|
||||
|
||||
mc.cleanup()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Closes the network connection and unsets internal variables. Do not call this
|
||||
// function after successfully authentication, call Close instead. This function
|
||||
// is called before auth or on auth failure because MySQL will have already
|
||||
// closed the network connection.
|
||||
func (mc *mysqlConn) cleanup() {
|
||||
if !mc.closed.TrySet(true) {
|
||||
return
|
||||
}
|
||||
|
||||
// Makes cleanup idempotent
|
||||
close(mc.closech)
|
||||
if mc.netConn == nil {
|
||||
return
|
||||
}
|
||||
if err := mc.netConn.Close(); err != nil {
|
||||
errLog.Print(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) error() error {
|
||||
if mc.closed.IsSet() {
|
||||
if err := mc.canceled.Value(); err != nil {
|
||||
return err
|
||||
}
|
||||
return ErrInvalidConn
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) {
|
||||
if mc.closed.IsSet() {
|
||||
errLog.Print(ErrInvalidConn)
|
||||
return nil, driver.ErrBadConn
|
||||
}
|
||||
// Send command
|
||||
err := mc.writeCommandPacketStr(comStmtPrepare, query)
|
||||
if err != nil {
|
||||
return nil, mc.markBadConn(err)
|
||||
}
|
||||
|
||||
stmt := &mysqlStmt{
|
||||
mc: mc,
|
||||
}
|
||||
|
||||
// Read Result
|
||||
columnCount, err := stmt.readPrepareResultPacket()
|
||||
if err == nil {
|
||||
if stmt.paramCount > 0 {
|
||||
if err = mc.readUntilEOF(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if columnCount > 0 {
|
||||
err = mc.readUntilEOF()
|
||||
}
|
||||
}
|
||||
|
||||
return stmt, err
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (string, error) {
|
||||
// Number of ? should be same to len(args)
|
||||
if strings.Count(query, "?") != len(args) {
|
||||
return "", driver.ErrSkip
|
||||
}
|
||||
|
||||
buf := mc.buf.takeCompleteBuffer()
|
||||
if buf == nil {
|
||||
// can not take the buffer. Something must be wrong with the connection
|
||||
errLog.Print(ErrBusyBuffer)
|
||||
return "", ErrInvalidConn
|
||||
}
|
||||
buf = buf[:0]
|
||||
argPos := 0
|
||||
|
||||
for i := 0; i < len(query); i++ {
|
||||
q := strings.IndexByte(query[i:], '?')
|
||||
if q == -1 {
|
||||
buf = append(buf, query[i:]...)
|
||||
break
|
||||
}
|
||||
buf = append(buf, query[i:i+q]...)
|
||||
i += q
|
||||
|
||||
arg := args[argPos]
|
||||
argPos++
|
||||
|
||||
if arg == nil {
|
||||
buf = append(buf, "NULL"...)
|
||||
continue
|
||||
}
|
||||
|
||||
switch v := arg.(type) {
|
||||
case int64:
|
||||
buf = strconv.AppendInt(buf, v, 10)
|
||||
case float64:
|
||||
buf = strconv.AppendFloat(buf, v, 'g', -1, 64)
|
||||
case bool:
|
||||
if v {
|
||||
buf = append(buf, '1')
|
||||
} else {
|
||||
buf = append(buf, '0')
|
||||
}
|
||||
case time.Time:
|
||||
if v.IsZero() {
|
||||
buf = append(buf, "'0000-00-00'"...)
|
||||
} else {
|
||||
v := v.In(mc.cfg.Loc)
|
||||
v = v.Add(time.Nanosecond * 500) // To round under microsecond
|
||||
year := v.Year()
|
||||
year100 := year / 100
|
||||
year1 := year % 100
|
||||
month := v.Month()
|
||||
day := v.Day()
|
||||
hour := v.Hour()
|
||||
minute := v.Minute()
|
||||
second := v.Second()
|
||||
micro := v.Nanosecond() / 1000
|
||||
|
||||
buf = append(buf, []byte{
|
||||
'\'',
|
||||
digits10[year100], digits01[year100],
|
||||
digits10[year1], digits01[year1],
|
||||
'-',
|
||||
digits10[month], digits01[month],
|
||||
'-',
|
||||
digits10[day], digits01[day],
|
||||
' ',
|
||||
digits10[hour], digits01[hour],
|
||||
':',
|
||||
digits10[minute], digits01[minute],
|
||||
':',
|
||||
digits10[second], digits01[second],
|
||||
}...)
|
||||
|
||||
if micro != 0 {
|
||||
micro10000 := micro / 10000
|
||||
micro100 := micro / 100 % 100
|
||||
micro1 := micro % 100
|
||||
buf = append(buf, []byte{
|
||||
'.',
|
||||
digits10[micro10000], digits01[micro10000],
|
||||
digits10[micro100], digits01[micro100],
|
||||
digits10[micro1], digits01[micro1],
|
||||
}...)
|
||||
}
|
||||
buf = append(buf, '\'')
|
||||
}
|
||||
case []byte:
|
||||
if v == nil {
|
||||
buf = append(buf, "NULL"...)
|
||||
} else {
|
||||
buf = append(buf, "_binary'"...)
|
||||
if mc.status&statusNoBackslashEscapes == 0 {
|
||||
buf = escapeBytesBackslash(buf, v)
|
||||
} else {
|
||||
buf = escapeBytesQuotes(buf, v)
|
||||
}
|
||||
buf = append(buf, '\'')
|
||||
}
|
||||
case string:
|
||||
buf = append(buf, '\'')
|
||||
if mc.status&statusNoBackslashEscapes == 0 {
|
||||
buf = escapeStringBackslash(buf, v)
|
||||
} else {
|
||||
buf = escapeStringQuotes(buf, v)
|
||||
}
|
||||
buf = append(buf, '\'')
|
||||
default:
|
||||
return "", driver.ErrSkip
|
||||
}
|
||||
|
||||
if len(buf)+4 > mc.maxAllowedPacket {
|
||||
return "", driver.ErrSkip
|
||||
}
|
||||
}
|
||||
if argPos != len(args) {
|
||||
return "", driver.ErrSkip
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) {
|
||||
if mc.closed.IsSet() {
|
||||
errLog.Print(ErrInvalidConn)
|
||||
return nil, driver.ErrBadConn
|
||||
}
|
||||
if len(args) != 0 {
|
||||
if !mc.cfg.InterpolateParams {
|
||||
return nil, driver.ErrSkip
|
||||
}
|
||||
// try to interpolate the parameters to save extra roundtrips for preparing and closing a statement
|
||||
prepared, err := mc.interpolateParams(query, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = prepared
|
||||
}
|
||||
mc.affectedRows = 0
|
||||
mc.insertId = 0
|
||||
|
||||
err := mc.exec(query)
|
||||
if err == nil {
|
||||
return &mysqlResult{
|
||||
affectedRows: int64(mc.affectedRows),
|
||||
insertId: int64(mc.insertId),
|
||||
}, err
|
||||
}
|
||||
return nil, mc.markBadConn(err)
|
||||
}
|
||||
|
||||
// Internal function to execute commands
|
||||
func (mc *mysqlConn) exec(query string) error {
|
||||
// Send command
|
||||
if err := mc.writeCommandPacketStr(comQuery, query); err != nil {
|
||||
return mc.markBadConn(err)
|
||||
}
|
||||
|
||||
// Read Result
|
||||
resLen, err := mc.readResultSetHeaderPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resLen > 0 {
|
||||
// columns
|
||||
if err := mc.readUntilEOF(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// rows
|
||||
if err := mc.readUntilEOF(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return mc.discardResults()
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, error) {
|
||||
return mc.query(query, args)
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) {
|
||||
if mc.closed.IsSet() {
|
||||
errLog.Print(ErrInvalidConn)
|
||||
return nil, driver.ErrBadConn
|
||||
}
|
||||
if len(args) != 0 {
|
||||
if !mc.cfg.InterpolateParams {
|
||||
return nil, driver.ErrSkip
|
||||
}
|
||||
// try client-side prepare to reduce roundtrip
|
||||
prepared, err := mc.interpolateParams(query, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = prepared
|
||||
}
|
||||
// Send command
|
||||
err := mc.writeCommandPacketStr(comQuery, query)
|
||||
if err == nil {
|
||||
// Read Result
|
||||
var resLen int
|
||||
resLen, err = mc.readResultSetHeaderPacket()
|
||||
if err == nil {
|
||||
rows := new(textRows)
|
||||
rows.mc = mc
|
||||
|
||||
if resLen == 0 {
|
||||
rows.rs.done = true
|
||||
|
||||
switch err := rows.NextResultSet(); err {
|
||||
case nil, io.EOF:
|
||||
return rows, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Columns
|
||||
rows.rs.columns, err = mc.readColumns(resLen)
|
||||
return rows, err
|
||||
}
|
||||
}
|
||||
return nil, mc.markBadConn(err)
|
||||
}
|
||||
|
||||
// Gets the value of the given MySQL System Variable
|
||||
// The returned byte slice is only valid until the next read
|
||||
func (mc *mysqlConn) getSystemVar(name string) ([]byte, error) {
|
||||
// Send command
|
||||
if err := mc.writeCommandPacketStr(comQuery, "SELECT @@"+name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read Result
|
||||
resLen, err := mc.readResultSetHeaderPacket()
|
||||
if err == nil {
|
||||
rows := new(textRows)
|
||||
rows.mc = mc
|
||||
rows.rs.columns = []mysqlField{{fieldType: fieldTypeVarChar}}
|
||||
|
||||
if resLen > 0 {
|
||||
// Columns
|
||||
if err := mc.readUntilEOF(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
dest := make([]driver.Value, resLen)
|
||||
if err = rows.readRow(dest); err == nil {
|
||||
return dest[0].([]byte), mc.readUntilEOF()
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// finish is called when the query has canceled.
|
||||
func (mc *mysqlConn) cancel(err error) {
|
||||
mc.canceled.Set(err)
|
||||
mc.cleanup()
|
||||
}
|
||||
|
||||
// finish is called when the query has succeeded.
|
||||
func (mc *mysqlConn) finish() {
|
||||
if !mc.watching || mc.finished == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case mc.finished <- struct{}{}:
|
||||
mc.watching = false
|
||||
case <-mc.closech:
|
||||
}
|
||||
}
|
||||
197
vendor/github.com/go-sql-driver/mysql/connection_go18.go
generated
vendored
@@ -1,197 +0,0 @@
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build go1.8
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
)
|
||||
|
||||
// Ping implements driver.Pinger interface
|
||||
func (mc *mysqlConn) Ping(ctx context.Context) error {
|
||||
if mc.closed.IsSet() {
|
||||
errLog.Print(ErrInvalidConn)
|
||||
return driver.ErrBadConn
|
||||
}
|
||||
|
||||
if err := mc.watchCancel(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer mc.finish()
|
||||
|
||||
if err := mc.writeCommandPacket(comPing); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mc.readResultOK(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeginTx implements driver.ConnBeginTx interface
|
||||
func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
|
||||
if err := mc.watchCancel(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer mc.finish()
|
||||
|
||||
if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault {
|
||||
level, err := mapIsolationLevel(opts.Isolation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = mc.exec("SET TRANSACTION ISOLATION LEVEL " + level)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return mc.begin(opts.ReadOnly)
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
|
||||
dargs, err := namedValueToValue(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := mc.watchCancel(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := mc.query(query, dargs)
|
||||
if err != nil {
|
||||
mc.finish()
|
||||
return nil, err
|
||||
}
|
||||
rows.finish = mc.finish
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
||||
dargs, err := namedValueToValue(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := mc.watchCancel(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer mc.finish()
|
||||
|
||||
return mc.Exec(query, dargs)
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
|
||||
if err := mc.watchCancel(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stmt, err := mc.Prepare(query)
|
||||
mc.finish()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
select {
|
||||
default:
|
||||
case <-ctx.Done():
|
||||
stmt.Close()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
return stmt, nil
|
||||
}
|
||||
|
||||
func (stmt *mysqlStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
|
||||
dargs, err := namedValueToValue(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := stmt.mc.watchCancel(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := stmt.query(dargs)
|
||||
if err != nil {
|
||||
stmt.mc.finish()
|
||||
return nil, err
|
||||
}
|
||||
rows.finish = stmt.mc.finish
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (stmt *mysqlStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
|
||||
dargs, err := namedValueToValue(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := stmt.mc.watchCancel(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.mc.finish()
|
||||
|
||||
return stmt.Exec(dargs)
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) watchCancel(ctx context.Context) error {
|
||||
if mc.watching {
|
||||
// Reach here if canceled,
|
||||
// so the connection is already invalid
|
||||
mc.cleanup()
|
||||
return nil
|
||||
}
|
||||
if ctx.Done() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
mc.watching = true
|
||||
select {
|
||||
default:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
if mc.watcher == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
mc.watcher <- ctx
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) startWatcher() {
|
||||
watcher := make(chan mysqlContext, 1)
|
||||
mc.watcher = watcher
|
||||
finished := make(chan struct{})
|
||||
mc.finished = finished
|
||||
go func() {
|
||||
for {
|
||||
var ctx mysqlContext
|
||||
select {
|
||||
case ctx = <-watcher:
|
||||
case <-mc.closech:
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
mc.cancel(ctx.Err())
|
||||
case <-finished:
|
||||
case <-mc.closech:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
166
vendor/github.com/go-sql-driver/mysql/const.go
generated
vendored
@@ -1,166 +0,0 @@
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package mysql
|
||||
|
||||
const (
|
||||
defaultMaxAllowedPacket = 4 << 20 // 4 MiB
|
||||
minProtocolVersion = 10
|
||||
maxPacketSize = 1<<24 - 1
|
||||
timeFormat = "2006-01-02 15:04:05.999999"
|
||||
)
|
||||
|
||||
// MySQL constants documentation:
|
||||
// http://dev.mysql.com/doc/internals/en/client-server-protocol.html
|
||||
|
||||
const (
|
||||
iOK byte = 0x00
|
||||
iLocalInFile byte = 0xfb
|
||||
iEOF byte = 0xfe
|
||||
iERR byte = 0xff
|
||||
)
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags
|
||||
type clientFlag uint32
|
||||
|
||||
const (
|
||||
clientLongPassword clientFlag = 1 << iota
|
||||
clientFoundRows
|
||||
clientLongFlag
|
||||
clientConnectWithDB
|
||||
clientNoSchema
|
||||
clientCompress
|
||||
clientODBC
|
||||
clientLocalFiles
|
||||
clientIgnoreSpace
|
||||
clientProtocol41
|
||||
clientInteractive
|
||||
clientSSL
|
||||
clientIgnoreSIGPIPE
|
||||
clientTransactions
|
||||
clientReserved
|
||||
clientSecureConn
|
||||
clientMultiStatements
|
||||
clientMultiResults
|
||||
clientPSMultiResults
|
||||
clientPluginAuth
|
||||
clientConnectAttrs
|
||||
clientPluginAuthLenEncClientData
|
||||
clientCanHandleExpiredPasswords
|
||||
clientSessionTrack
|
||||
clientDeprecateEOF
|
||||
)
|
||||
|
||||
const (
|
||||
comQuit byte = iota + 1
|
||||
comInitDB
|
||||
comQuery
|
||||
comFieldList
|
||||
comCreateDB
|
||||
comDropDB
|
||||
comRefresh
|
||||
comShutdown
|
||||
comStatistics
|
||||
comProcessInfo
|
||||
comConnect
|
||||
comProcessKill
|
||||
comDebug
|
||||
comPing
|
||||
comTime
|
||||
comDelayedInsert
|
||||
comChangeUser
|
||||
comBinlogDump
|
||||
comTableDump
|
||||
comConnectOut
|
||||
comRegisterSlave
|
||||
comStmtPrepare
|
||||
comStmtExecute
|
||||
comStmtSendLongData
|
||||
comStmtClose
|
||||
comStmtReset
|
||||
comSetOption
|
||||
comStmtFetch
|
||||
)
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-Protocol::ColumnType
|
||||
type fieldType byte
|
||||
|
||||
const (
|
||||
fieldTypeDecimal fieldType = iota
|
||||
fieldTypeTiny
|
||||
fieldTypeShort
|
||||
fieldTypeLong
|
||||
fieldTypeFloat
|
||||
fieldTypeDouble
|
||||
fieldTypeNULL
|
||||
fieldTypeTimestamp
|
||||
fieldTypeLongLong
|
||||
fieldTypeInt24
|
||||
fieldTypeDate
|
||||
fieldTypeTime
|
||||
fieldTypeDateTime
|
||||
fieldTypeYear
|
||||
fieldTypeNewDate
|
||||
fieldTypeVarChar
|
||||
fieldTypeBit
|
||||
)
|
||||
const (
|
||||
fieldTypeJSON fieldType = iota + 0xf5
|
||||
fieldTypeNewDecimal
|
||||
fieldTypeEnum
|
||||
fieldTypeSet
|
||||
fieldTypeTinyBLOB
|
||||
fieldTypeMediumBLOB
|
||||
fieldTypeLongBLOB
|
||||
fieldTypeBLOB
|
||||
fieldTypeVarString
|
||||
fieldTypeString
|
||||
fieldTypeGeometry
|
||||
)
|
||||
|
||||
type fieldFlag uint16
|
||||
|
||||
const (
|
||||
flagNotNULL fieldFlag = 1 << iota
|
||||
flagPriKey
|
||||
flagUniqueKey
|
||||
flagMultipleKey
|
||||
flagBLOB
|
||||
flagUnsigned
|
||||
flagZeroFill
|
||||
flagBinary
|
||||
flagEnum
|
||||
flagAutoIncrement
|
||||
flagTimestamp
|
||||
flagSet
|
||||
flagUnknown1
|
||||
flagUnknown2
|
||||
flagUnknown3
|
||||
flagUnknown4
|
||||
)
|
||||
|
||||
// http://dev.mysql.com/doc/internals/en/status-flags.html
|
||||
type statusFlag uint16
|
||||
|
||||
const (
|
||||
statusInTrans statusFlag = 1 << iota
|
||||
statusInAutocommit
|
||||
statusReserved // Not in documentation
|
||||
statusMoreResultsExists
|
||||
statusNoGoodIndexUsed
|
||||
statusNoIndexUsed
|
||||
statusCursorExists
|
||||
statusLastRowSent
|
||||
statusDbDropped
|
||||
statusNoBackslashEscapes
|
||||
statusMetadataChanged
|
||||
statusQueryWasSlow
|
||||
statusPsOutParams
|
||||
statusInTransReadonly
|
||||
statusSessionStateChanged
|
||||
)
|
||||
193
vendor/github.com/go-sql-driver/mysql/driver.go
generated
vendored
@@ -1,193 +0,0 @@
|
||||
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Package mysql provides a MySQL driver for Go's database/sql package.
|
||||
//
|
||||
// The driver should be used via the database/sql package:
|
||||
//
|
||||
// import "database/sql"
|
||||
// import _ "github.com/go-sql-driver/mysql"
|
||||
//
|
||||
// db, err := sql.Open("mysql", "user:password@/dbname")
|
||||
//
|
||||
// See https://github.com/go-sql-driver/mysql#usage for details
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"net"
|
||||
)
|
||||
|
||||
// watcher interface is used for context support (From Go 1.8)
|
||||
type watcher interface {
|
||||
startWatcher()
|
||||
}
|
||||
|
||||
// MySQLDriver is exported to make the driver directly accessible.
|
||||
// In general the driver is used via the database/sql package.
|
||||
type MySQLDriver struct{}
|
||||
|
||||
// DialFunc is a function which can be used to establish the network connection.
|
||||
// Custom dial functions must be registered with RegisterDial
|
||||
type DialFunc func(addr string) (net.Conn, error)
|
||||
|
||||
var dials map[string]DialFunc
|
||||
|
||||
// RegisterDial registers a custom dial function. It can then be used by the
|
||||
// network address mynet(addr), where mynet is the registered new network.
|
||||
// addr is passed as a parameter to the dial function.
|
||||
func RegisterDial(net string, dial DialFunc) {
|
||||
if dials == nil {
|
||||
dials = make(map[string]DialFunc)
|
||||
}
|
||||
dials[net] = dial
|
||||
}
|
||||
|
||||
// Open new Connection.
|
||||
// See https://github.com/go-sql-driver/mysql#dsn-data-source-name for how
|
||||
// the DSN string is formated
|
||||
func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
|
||||
var err error
|
||||
|
||||
// New mysqlConn
|
||||
mc := &mysqlConn{
|
||||
maxAllowedPacket: maxPacketSize,
|
||||
maxWriteSize: maxPacketSize - 1,
|
||||
closech: make(chan struct{}),
|
||||
}
|
||||
mc.cfg, err = ParseDSN(dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mc.parseTime = mc.cfg.ParseTime
|
||||
|
||||
// Connect to Server
|
||||
if dial, ok := dials[mc.cfg.Net]; ok {
|
||||
mc.netConn, err = dial(mc.cfg.Addr)
|
||||
} else {
|
||||
nd := net.Dialer{Timeout: mc.cfg.Timeout}
|
||||
mc.netConn, err = nd.Dial(mc.cfg.Net, mc.cfg.Addr)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enable TCP Keepalives on TCP connections
|
||||
if tc, ok := mc.netConn.(*net.TCPConn); ok {
|
||||
if err := tc.SetKeepAlive(true); err != nil {
|
||||
// Don't send COM_QUIT before handshake.
|
||||
mc.netConn.Close()
|
||||
mc.netConn = nil
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Call startWatcher for context support (From Go 1.8)
|
||||
if s, ok := interface{}(mc).(watcher); ok {
|
||||
s.startWatcher()
|
||||
}
|
||||
|
||||
mc.buf = newBuffer(mc.netConn)
|
||||
|
||||
// Set I/O timeouts
|
||||
mc.buf.timeout = mc.cfg.ReadTimeout
|
||||
mc.writeTimeout = mc.cfg.WriteTimeout
|
||||
|
||||
// Reading Handshake Initialization Packet
|
||||
cipher, err := mc.readInitPacket()
|
||||
if err != nil {
|
||||
mc.cleanup()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Send Client Authentication Packet
|
||||
if err = mc.writeAuthPacket(cipher); err != nil {
|
||||
mc.cleanup()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Handle response to auth packet, switch methods if possible
|
||||
if err = handleAuthResult(mc, cipher); err != nil {
|
||||
// Authentication failed and MySQL has already closed the connection
|
||||
// (https://dev.mysql.com/doc/internals/en/authentication-fails.html).
|
||||
// Do not send COM_QUIT, just cleanup and return the error.
|
||||
mc.cleanup()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if mc.cfg.MaxAllowedPacket > 0 {
|
||||
mc.maxAllowedPacket = mc.cfg.MaxAllowedPacket
|
||||
} else {
|
||||
// Get max allowed packet size
|
||||
maxap, err := mc.getSystemVar("max_allowed_packet")
|
||||
if err != nil {
|
||||
mc.Close()
|
||||
return nil, err
|
||||
}
|
||||
mc.maxAllowedPacket = stringToInt(maxap) - 1
|
||||
}
|
||||
if mc.maxAllowedPacket < maxPacketSize {
|
||||
mc.maxWriteSize = mc.maxAllowedPacket
|
||||
}
|
||||
|
||||
// Handle DSN Params
|
||||
err = mc.handleParams()
|
||||
if err != nil {
|
||||
mc.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mc, nil
|
||||
}
|
||||
|
||||
func handleAuthResult(mc *mysqlConn, oldCipher []byte) error {
|
||||
// Read Result Packet
|
||||
cipher, err := mc.readResultOK()
|
||||
if err == nil {
|
||||
return nil // auth successful
|
||||
}
|
||||
|
||||
if mc.cfg == nil {
|
||||
return err // auth failed and retry not possible
|
||||
}
|
||||
|
||||
// Retry auth if configured to do so.
|
||||
if mc.cfg.AllowOldPasswords && err == ErrOldPassword {
|
||||
// Retry with old authentication method. Note: there are edge cases
|
||||
// where this should work but doesn't; this is currently "wontfix":
|
||||
// https://github.com/go-sql-driver/mysql/issues/184
|
||||
|
||||
// If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is
|
||||
// sent and we have to keep using the cipher sent in the init packet.
|
||||
if cipher == nil {
|
||||
cipher = oldCipher
|
||||
}
|
||||
|
||||
if err = mc.writeOldAuthPacket(cipher); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mc.readResultOK()
|
||||
} else if mc.cfg.AllowCleartextPasswords && err == ErrCleartextPassword {
|
||||
// Retry with clear text password for
|
||||
// http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html
|
||||
// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
|
||||
if err = mc.writeClearAuthPacket(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mc.readResultOK()
|
||||
} else if mc.cfg.AllowNativePasswords && err == ErrNativePassword {
|
||||
if err = mc.writeNativeAuthPacket(cipher); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mc.readResultOK()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func init() {
|
||||
sql.Register("mysql", &MySQLDriver{})
|
||||
}
|
||||
587
vendor/github.com/go-sql-driver/mysql/dsn.go
generated
vendored
@@ -1,587 +0,0 @@
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2016 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidDSNUnescaped = errors.New("invalid DSN: did you forget to escape a param value?")
|
||||
errInvalidDSNAddr = errors.New("invalid DSN: network address not terminated (missing closing brace)")
|
||||
errInvalidDSNNoSlash = errors.New("invalid DSN: missing the slash separating the database name")
|
||||
errInvalidDSNUnsafeCollation = errors.New("invalid DSN: interpolateParams can not be used with unsafe collations")
|
||||
)
|
||||
|
||||
// Config is a configuration parsed from a DSN string.
|
||||
// If a new Config is created instead of being parsed from a DSN string,
|
||||
// the NewConfig function should be used, which sets default values.
|
||||
type Config struct {
|
||||
User string // Username
|
||||
Passwd string // Password (requires User)
|
||||
Net string // Network type
|
||||
Addr string // Network address (requires Net)
|
||||
DBName string // Database name
|
||||
Params map[string]string // Connection parameters
|
||||
Collation string // Connection collation
|
||||
Loc *time.Location // Location for time.Time values
|
||||
MaxAllowedPacket int // Max packet size allowed
|
||||
TLSConfig string // TLS configuration name
|
||||
tls *tls.Config // TLS configuration
|
||||
Timeout time.Duration // Dial timeout
|
||||
ReadTimeout time.Duration // I/O read timeout
|
||||
WriteTimeout time.Duration // I/O write timeout
|
||||
|
||||
AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE
|
||||
AllowCleartextPasswords bool // Allows the cleartext client side plugin
|
||||
AllowNativePasswords bool // Allows the native password authentication method
|
||||
AllowOldPasswords bool // Allows the old insecure password method
|
||||
ClientFoundRows bool // Return number of matching rows instead of rows changed
|
||||
ColumnsWithAlias bool // Prepend table alias to column names
|
||||
InterpolateParams bool // Interpolate placeholders into query string
|
||||
MultiStatements bool // Allow multiple statements in one query
|
||||
ParseTime bool // Parse time values to time.Time
|
||||
RejectReadOnly bool // Reject read-only connections
|
||||
}
|
||||
|
||||
// NewConfig creates a new Config and sets default values.
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
Collation: defaultCollation,
|
||||
Loc: time.UTC,
|
||||
MaxAllowedPacket: defaultMaxAllowedPacket,
|
||||
AllowNativePasswords: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) normalize() error {
|
||||
if cfg.InterpolateParams && unsafeCollations[cfg.Collation] {
|
||||
return errInvalidDSNUnsafeCollation
|
||||
}
|
||||
|
||||
// Set default network if empty
|
||||
if cfg.Net == "" {
|
||||
cfg.Net = "tcp"
|
||||
}
|
||||
|
||||
// Set default address if empty
|
||||
if cfg.Addr == "" {
|
||||
switch cfg.Net {
|
||||
case "tcp":
|
||||
cfg.Addr = "127.0.0.1:3306"
|
||||
case "unix":
|
||||
cfg.Addr = "/tmp/mysql.sock"
|
||||
default:
|
||||
return errors.New("default addr for network '" + cfg.Net + "' unknown")
|
||||
}
|
||||
|
||||
} else if cfg.Net == "tcp" {
|
||||
cfg.Addr = ensureHavePort(cfg.Addr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormatDSN formats the given Config into a DSN string which can be passed to
|
||||
// the driver.
|
||||
func (cfg *Config) FormatDSN() string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
// [username[:password]@]
|
||||
if len(cfg.User) > 0 {
|
||||
buf.WriteString(cfg.User)
|
||||
if len(cfg.Passwd) > 0 {
|
||||
buf.WriteByte(':')
|
||||
buf.WriteString(cfg.Passwd)
|
||||
}
|
||||
buf.WriteByte('@')
|
||||
}
|
||||
|
||||
// [protocol[(address)]]
|
||||
if len(cfg.Net) > 0 {
|
||||
buf.WriteString(cfg.Net)
|
||||
if len(cfg.Addr) > 0 {
|
||||
buf.WriteByte('(')
|
||||
buf.WriteString(cfg.Addr)
|
||||
buf.WriteByte(')')
|
||||
}
|
||||
}
|
||||
|
||||
// /dbname
|
||||
buf.WriteByte('/')
|
||||
buf.WriteString(cfg.DBName)
|
||||
|
||||
// [?param1=value1&...¶mN=valueN]
|
||||
hasParam := false
|
||||
|
||||
if cfg.AllowAllFiles {
|
||||
hasParam = true
|
||||
buf.WriteString("?allowAllFiles=true")
|
||||
}
|
||||
|
||||
if cfg.AllowCleartextPasswords {
|
||||
if hasParam {
|
||||
buf.WriteString("&allowCleartextPasswords=true")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?allowCleartextPasswords=true")
|
||||
}
|
||||
}
|
||||
|
||||
if !cfg.AllowNativePasswords {
|
||||
if hasParam {
|
||||
buf.WriteString("&allowNativePasswords=false")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?allowNativePasswords=false")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.AllowOldPasswords {
|
||||
if hasParam {
|
||||
buf.WriteString("&allowOldPasswords=true")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?allowOldPasswords=true")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.ClientFoundRows {
|
||||
if hasParam {
|
||||
buf.WriteString("&clientFoundRows=true")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?clientFoundRows=true")
|
||||
}
|
||||
}
|
||||
|
||||
if col := cfg.Collation; col != defaultCollation && len(col) > 0 {
|
||||
if hasParam {
|
||||
buf.WriteString("&collation=")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?collation=")
|
||||
}
|
||||
buf.WriteString(col)
|
||||
}
|
||||
|
||||
if cfg.ColumnsWithAlias {
|
||||
if hasParam {
|
||||
buf.WriteString("&columnsWithAlias=true")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?columnsWithAlias=true")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.InterpolateParams {
|
||||
if hasParam {
|
||||
buf.WriteString("&interpolateParams=true")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?interpolateParams=true")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Loc != time.UTC && cfg.Loc != nil {
|
||||
if hasParam {
|
||||
buf.WriteString("&loc=")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?loc=")
|
||||
}
|
||||
buf.WriteString(url.QueryEscape(cfg.Loc.String()))
|
||||
}
|
||||
|
||||
if cfg.MultiStatements {
|
||||
if hasParam {
|
||||
buf.WriteString("&multiStatements=true")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?multiStatements=true")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.ParseTime {
|
||||
if hasParam {
|
||||
buf.WriteString("&parseTime=true")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?parseTime=true")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.ReadTimeout > 0 {
|
||||
if hasParam {
|
||||
buf.WriteString("&readTimeout=")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?readTimeout=")
|
||||
}
|
||||
buf.WriteString(cfg.ReadTimeout.String())
|
||||
}
|
||||
|
||||
if cfg.RejectReadOnly {
|
||||
if hasParam {
|
||||
buf.WriteString("&rejectReadOnly=true")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?rejectReadOnly=true")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Timeout > 0 {
|
||||
if hasParam {
|
||||
buf.WriteString("&timeout=")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?timeout=")
|
||||
}
|
||||
buf.WriteString(cfg.Timeout.String())
|
||||
}
|
||||
|
||||
if len(cfg.TLSConfig) > 0 {
|
||||
if hasParam {
|
||||
buf.WriteString("&tls=")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?tls=")
|
||||
}
|
||||
buf.WriteString(url.QueryEscape(cfg.TLSConfig))
|
||||
}
|
||||
|
||||
if cfg.WriteTimeout > 0 {
|
||||
if hasParam {
|
||||
buf.WriteString("&writeTimeout=")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?writeTimeout=")
|
||||
}
|
||||
buf.WriteString(cfg.WriteTimeout.String())
|
||||
}
|
||||
|
||||
if cfg.MaxAllowedPacket != defaultMaxAllowedPacket {
|
||||
if hasParam {
|
||||
buf.WriteString("&maxAllowedPacket=")
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteString("?maxAllowedPacket=")
|
||||
}
|
||||
buf.WriteString(strconv.Itoa(cfg.MaxAllowedPacket))
|
||||
|
||||
}
|
||||
|
||||
// other params
|
||||
if cfg.Params != nil {
|
||||
var params []string
|
||||
for param := range cfg.Params {
|
||||
params = append(params, param)
|
||||
}
|
||||
sort.Strings(params)
|
||||
for _, param := range params {
|
||||
if hasParam {
|
||||
buf.WriteByte('&')
|
||||
} else {
|
||||
hasParam = true
|
||||
buf.WriteByte('?')
|
||||
}
|
||||
|
||||
buf.WriteString(param)
|
||||
buf.WriteByte('=')
|
||||
buf.WriteString(url.QueryEscape(cfg.Params[param]))
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// ParseDSN parses the DSN string to a Config
|
||||
func ParseDSN(dsn string) (cfg *Config, err error) {
|
||||
// New config with some default values
|
||||
cfg = NewConfig()
|
||||
|
||||
// [user[:password]@][net[(addr)]]/dbname[?param1=value1¶mN=valueN]
|
||||
// Find the last '/' (since the password or the net addr might contain a '/')
|
||||
foundSlash := false
|
||||
for i := len(dsn) - 1; i >= 0; i-- {
|
||||
if dsn[i] == '/' {
|
||||
foundSlash = true
|
||||
var j, k int
|
||||
|
||||
// left part is empty if i <= 0
|
||||
if i > 0 {
|
||||
// [username[:password]@][protocol[(address)]]
|
||||
// Find the last '@' in dsn[:i]
|
||||
for j = i; j >= 0; j-- {
|
||||
if dsn[j] == '@' {
|
||||
// username[:password]
|
||||
// Find the first ':' in dsn[:j]
|
||||
for k = 0; k < j; k++ {
|
||||
if dsn[k] == ':' {
|
||||
cfg.Passwd = dsn[k+1 : j]
|
||||
break
|
||||
}
|
||||
}
|
||||
cfg.User = dsn[:k]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// [protocol[(address)]]
|
||||
// Find the first '(' in dsn[j+1:i]
|
||||
for k = j + 1; k < i; k++ {
|
||||
if dsn[k] == '(' {
|
||||
// dsn[i-1] must be == ')' if an address is specified
|
||||
if dsn[i-1] != ')' {
|
||||
if strings.ContainsRune(dsn[k+1:i], ')') {
|
||||
return nil, errInvalidDSNUnescaped
|
||||
}
|
||||
return nil, errInvalidDSNAddr
|
||||
}
|
||||
cfg.Addr = dsn[k+1 : i-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
cfg.Net = dsn[j+1 : k]
|
||||
}
|
||||
|
||||
// dbname[?param1=value1&...¶mN=valueN]
|
||||
// Find the first '?' in dsn[i+1:]
|
||||
for j = i + 1; j < len(dsn); j++ {
|
||||
if dsn[j] == '?' {
|
||||
if err = parseDSNParams(cfg, dsn[j+1:]); err != nil {
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
cfg.DBName = dsn[i+1 : j]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundSlash && len(dsn) > 0 {
|
||||
return nil, errInvalidDSNNoSlash
|
||||
}
|
||||
|
||||
if err = cfg.normalize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// parseDSNParams parses the DSN "query string"
|
||||
// Values must be url.QueryEscape'ed
|
||||
func parseDSNParams(cfg *Config, params string) (err error) {
|
||||
for _, v := range strings.Split(params, "&") {
|
||||
param := strings.SplitN(v, "=", 2)
|
||||
if len(param) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// cfg params
|
||||
switch value := param[1]; param[0] {
|
||||
|
||||
// Disable INFILE whitelist / enable all files
|
||||
case "allowAllFiles":
|
||||
var isBool bool
|
||||
cfg.AllowAllFiles, isBool = readBool(value)
|
||||
if !isBool {
|
||||
return errors.New("invalid bool value: " + value)
|
||||
}
|
||||
|
||||
// Use cleartext authentication mode (MySQL 5.5.10+)
|
||||
case "allowCleartextPasswords":
|
||||
var isBool bool
|
||||
cfg.AllowCleartextPasswords, isBool = readBool(value)
|
||||
if !isBool {
|
||||
return errors.New("invalid bool value: " + value)
|
||||
}
|
||||
|
||||
// Use native password authentication
|
||||
case "allowNativePasswords":
|
||||
var isBool bool
|
||||
cfg.AllowNativePasswords, isBool = readBool(value)
|
||||
if !isBool {
|
||||
return errors.New("invalid bool value: " + value)
|
||||
}
|
||||
|
||||
// Use old authentication mode (pre MySQL 4.1)
|
||||
case "allowOldPasswords":
|
||||
var isBool bool
|
||||
cfg.AllowOldPasswords, isBool = readBool(value)
|
||||
if !isBool {
|
||||
return errors.New("invalid bool value: " + value)
|
||||
}
|
||||
|
||||
// Switch "rowsAffected" mode
|
||||
case "clientFoundRows":
|
||||
var isBool bool
|
||||
cfg.ClientFoundRows, isBool = readBool(value)
|
||||
if !isBool {
|
||||
return errors.New("invalid bool value: " + value)
|
||||
}
|
||||
|
||||
// Collation
|
||||
case "collation":
|
||||
cfg.Collation = value
|
||||
break
|
||||
|
||||
case "columnsWithAlias":
|
||||
var isBool bool
|
||||
cfg.ColumnsWithAlias, isBool = readBool(value)
|
||||
if !isBool {
|
||||
return errors.New("invalid bool value: " + value)
|
||||
}
|
||||
|
||||
// Compression
|
||||
case "compress":
|
||||
return errors.New("compression not implemented yet")
|
||||
|
||||
// Enable client side placeholder substitution
|
||||
case "interpolateParams":
|
||||
var isBool bool
|
||||
cfg.InterpolateParams, isBool = readBool(value)
|
||||
if !isBool {
|
||||
return errors.New("invalid bool value: " + value)
|
||||
}
|
||||
|
||||
// Time Location
|
||||
case "loc":
|
||||
if value, err = url.QueryUnescape(value); err != nil {
|
||||
return
|
||||
}
|
||||
cfg.Loc, err = time.LoadLocation(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// multiple statements in one query
|
||||
case "multiStatements":
|
||||
var isBool bool
|
||||
cfg.MultiStatements, isBool = readBool(value)
|
||||
if !isBool {
|
||||
return errors.New("invalid bool value: " + value)
|
||||
}
|
||||
|
||||
// time.Time parsing
|
||||
case "parseTime":
|
||||
var isBool bool
|
||||
cfg.ParseTime, isBool = readBool(value)
|
||||
if !isBool {
|
||||
return errors.New("invalid bool value: " + value)
|
||||
}
|
||||
|
||||
// I/O read Timeout
|
||||
case "readTimeout":
|
||||
cfg.ReadTimeout, err = time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Reject read-only connections
|
||||
case "rejectReadOnly":
|
||||
var isBool bool
|
||||
cfg.RejectReadOnly, isBool = readBool(value)
|
||||
if !isBool {
|
||||
return errors.New("invalid bool value: " + value)
|
||||
}
|
||||
|
||||
// Strict mode
|
||||
case "strict":
|
||||
panic("strict mode has been removed. See https://github.com/go-sql-driver/mysql/wiki/strict-mode")
|
||||
|
||||
// Dial Timeout
|
||||
case "timeout":
|
||||
cfg.Timeout, err = time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TLS-Encryption
|
||||
case "tls":
|
||||
boolValue, isBool := readBool(value)
|
||||
if isBool {
|
||||
if boolValue {
|
||||
cfg.TLSConfig = "true"
|
||||
cfg.tls = &tls.Config{}
|
||||
host, _, err := net.SplitHostPort(cfg.Addr)
|
||||
if err == nil {
|
||||
cfg.tls.ServerName = host
|
||||
}
|
||||
} else {
|
||||
cfg.TLSConfig = "false"
|
||||
}
|
||||
} else if vl := strings.ToLower(value); vl == "skip-verify" {
|
||||
cfg.TLSConfig = vl
|
||||
cfg.tls = &tls.Config{InsecureSkipVerify: true}
|
||||
} else {
|
||||
name, err := url.QueryUnescape(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid value for TLS config name: %v", err)
|
||||
}
|
||||
|
||||
if tlsConfig := getTLSConfigClone(name); tlsConfig != nil {
|
||||
if len(tlsConfig.ServerName) == 0 && !tlsConfig.InsecureSkipVerify {
|
||||
host, _, err := net.SplitHostPort(cfg.Addr)
|
||||
if err == nil {
|
||||
tlsConfig.ServerName = host
|
||||
}
|
||||
}
|
||||
|
||||
cfg.TLSConfig = name
|
||||
cfg.tls = tlsConfig
|
||||
} else {
|
||||
return errors.New("invalid value / unknown config name: " + name)
|
||||
}
|
||||
}
|
||||
|
||||
// I/O write Timeout
|
||||
case "writeTimeout":
|
||||
cfg.WriteTimeout, err = time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case "maxAllowedPacket":
|
||||
cfg.MaxAllowedPacket, err = strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
default:
|
||||
// lazy init
|
||||
if cfg.Params == nil {
|
||||
cfg.Params = make(map[string]string)
|
||||
}
|
||||
|
||||
if cfg.Params[param[0]], err = url.QueryUnescape(value); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ensureHavePort(addr string) string {
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||
return net.JoinHostPort(addr, "3306")
|
||||
}
|
||||
return addr
|
||||
}
|
||||
65
vendor/github.com/go-sql-driver/mysql/errors.go
generated
vendored
@@ -1,65 +0,0 @@
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Various errors the driver might return. Can change between driver versions.
|
||||
var (
|
||||
ErrInvalidConn = errors.New("invalid connection")
|
||||
ErrMalformPkt = errors.New("malformed packet")
|
||||
ErrNoTLS = errors.New("TLS requested but server does not support TLS")
|
||||
ErrCleartextPassword = errors.New("this user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN")
|
||||
ErrNativePassword = errors.New("this user requires mysql native password authentication.")
|
||||
ErrOldPassword = errors.New("this user requires old password authentication. If you still want to use it, please add 'allowOldPasswords=1' to your DSN. See also https://github.com/go-sql-driver/mysql/wiki/old_passwords")
|
||||
ErrUnknownPlugin = errors.New("this authentication plugin is not supported")
|
||||
ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+")
|
||||
ErrPktSync = errors.New("commands out of sync. You can't run this command now")
|
||||
ErrPktSyncMul = errors.New("commands out of sync. Did you run multiple statements at once?")
|
||||
ErrPktTooLarge = errors.New("packet for query is too large. Try adjusting the 'max_allowed_packet' variable on the server")
|
||||
ErrBusyBuffer = errors.New("busy buffer")
|
||||
|
||||
// errBadConnNoWrite is used for connection errors where nothing was sent to the database yet.
|
||||
// If this happens first in a function starting a database interaction, it should be replaced by driver.ErrBadConn
|
||||
// to trigger a resend.
|
||||
// See https://github.com/go-sql-driver/mysql/pull/302
|
||||
errBadConnNoWrite = errors.New("bad connection")
|
||||
)
|
||||
|
||||
var errLog = Logger(log.New(os.Stderr, "[mysql] ", log.Ldate|log.Ltime|log.Lshortfile))
|
||||
|
||||
// Logger is used to log critical error messages.
|
||||
type Logger interface {
|
||||
Print(v ...interface{})
|
||||
}
|
||||
|
||||
// SetLogger is used to set the logger for critical errors.
|
||||
// The initial logger is os.Stderr.
|
||||
func SetLogger(logger Logger) error {
|
||||
if logger == nil {
|
||||
return errors.New("logger is nil")
|
||||
}
|
||||
errLog = logger
|
||||
return nil
|
||||
}
|
||||
|
||||
// MySQLError is an error type which represents a single MySQL error
|
||||
type MySQLError struct {
|
||||
Number uint16
|
||||
Message string
|
||||
}
|
||||
|
||||
func (me *MySQLError) Error() string {
|
||||
return fmt.Sprintf("Error %d: %s", me.Number, me.Message)
|
||||
}
|
||||