Compare commits
163 Commits
v1.2.0
...
guardrails
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26a7186f40 | ||
|
|
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 |
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];
|
||||
}
|
||||
}
|
||||
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 {
|
||||
rankdir=LR;
|
||||
layout=dot;
|
||||
node[shape=record];
|
||||
|
||||
start[label="ssh sshportal";color=blue;fontcolor=blue;fontsize=20];
|
||||
|
||||
subgraph cluster_sshportal {
|
||||
graph[fontsize=20;style=dashed;color=purple;fontcolor=purple];
|
||||
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 shell"];
|
||||
ssh_proxy[label="SSH proxy"];
|
||||
learn_key[label="learn key"];
|
||||
}
|
||||
err_and_exit[label="error and exit";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=20;style=dashed;color=purple;fontcolor=purple];
|
||||
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[headlabel="user=invite:<token>"];
|
||||
}
|
||||
{
|
||||
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"];
|
||||
invite_manager -> err_and_exit[label="invalid token"];
|
||||
}
|
||||
}
|
||||
188
.assets/flow-diagram.svg
Normal file
@@ -0,0 +1,188 @@
|
||||
<?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="1026pt" height="312pt"
|
||||
viewBox="0.00 0.00 1026.42 312.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 308)">
|
||||
<title>%3</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-308 1022.4219,-308 1022.4219,4 -4,4"/>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_sshportal</title>
|
||||
<polygon fill="none" stroke="#a020f0" stroke-dasharray="5,2" points="147.7832,-8 147.7832,-296 858.9775,-296 858.9775,-8 147.7832,-8"/>
|
||||
<text text-anchor="middle" x="503.3804" y="-276" font-family="Times,serif" font-size="20.00" fill="#a020f0">sshportal</text>
|
||||
</g>
|
||||
<g id="clust6" class="cluster">
|
||||
<title>cluster_hosts</title>
|
||||
<polygon fill="none" stroke="#a020f0" stroke-dasharray="5,2" points="879.9775,-104 879.9775,-296 1010.4219,-296 1010.4219,-104 879.9775,-104"/>
|
||||
<text text-anchor="middle" x="945.1997" y="-276" font-family="Times,serif" font-size="20.00" fill="#a020f0">your hosts</text>
|
||||
</g>
|
||||
<!-- start -->
|
||||
<g id="node1" class="node">
|
||||
<title>start</title>
|
||||
<polygon fill="none" stroke="#0000ff" points="0,-118 0,-154 118.7832,-154 118.7832,-118 0,-118"/>
|
||||
<text text-anchor="middle" x="59.3916" y="-130" font-family="Times,serif" font-size="20.00" fill="#0000ff">ssh sshportal</text>
|
||||
</g>
|
||||
<!-- known_user_key -->
|
||||
<g id="node2" class="node">
|
||||
<title>known_user_key</title>
|
||||
<polygon fill="none" stroke="#ff8c00" points="162.7832,-157 162.7832,-193 267.4316,-193 267.4316,-157 162.7832,-157"/>
|
||||
<text text-anchor="middle" x="215.1074" y="-170.8" font-family="Times,serif" 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="M119.1501,-150.9669C130.1162,-153.7134 141.5894,-156.587 152.6326,-159.3528"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="152.0758,-162.8214 162.6266,-161.8558 153.7765,-156.0311 152.0758,-162.8214"/>
|
||||
</g>
|
||||
<!-- unknown_user_key -->
|
||||
<g id="node3" class="node">
|
||||
<title>unknown_user_key</title>
|
||||
<polygon fill="none" stroke="#ff8c00" points="155.7832,-72 155.7832,-108 274.4316,-108 274.4316,-72 155.7832,-72"/>
|
||||
<text text-anchor="middle" x="215.1074" y="-85.8" font-family="Times,serif" 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="M119.1501,-118.3468C127.968,-115.7419 137.1138,-113.0401 146.1003,-110.3854"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="147.1673,-113.7198 155.766,-107.5301 145.1841,-107.0066 147.1673,-113.7198"/>
|
||||
</g>
|
||||
<!-- acl_manager -->
|
||||
<g id="node5" class="node">
|
||||
<title>acl_manager</title>
|
||||
<polygon fill="none" stroke="#ff8c00" points="514.7056,-173 514.7056,-209 609.8862,-209 609.8862,-173 514.7056,-173"/>
|
||||
<text text-anchor="middle" x="562.2959" y="-186.8" font-family="Times,serif" 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="M267.461,-177.4127C331.1153,-180.3462 438.21,-185.2816 504.3082,-188.3277"/>
|
||||
<polygon fill="#ff8c00" stroke="#ff8c00" points="504.401,-191.8356 514.5516,-188.7997 504.7233,-184.843 504.401,-191.8356"/>
|
||||
<text text-anchor="middle" x="393.4697" y="-188.8" font-family="Times,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>
|
||||
<polygon fill="none" stroke="#006400" points="761.6929,-223 761.6929,-259 848.855,-259 848.855,-223 761.6929,-223"/>
|
||||
<text text-anchor="middle" x="805.2739" y="-236.8" font-family="Times,serif" font-size="14.00" fill="#006400">built-in 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="M267.592,-193.0548C281.6792,-197.2785 297.0081,-201.3215 311.4316,-204 469.5409,-233.361 660.2348,-239.5693 751.4965,-240.7835"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="751.5568,-244.2844 761.5974,-240.9027 751.6394,-237.2848 751.5568,-244.2844"/>
|
||||
<text text-anchor="middle" x="562.2959" y="-238.8" font-family="Times,serif" font-size="14.00" fill="#006400">user=admin</text>
|
||||
</g>
|
||||
<!-- err_and_exit -->
|
||||
<g id="node9" class="node">
|
||||
<title>err_and_exit</title>
|
||||
<polygon fill="none" stroke="#ff0000" points="759.5703,-106 759.5703,-142 850.9775,-142 850.9775,-106 759.5703,-106"/>
|
||||
<text text-anchor="middle" x="805.2739" y="-119.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">error 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="M267.4808,-170.4741C378.1362,-160.9117 634.8943,-138.7236 748.9418,-128.868"/>
|
||||
<polygon fill="#ff0000" stroke="#ff0000" points="749.5354,-132.3298 759.1969,-127.9818 748.9327,-125.3558 749.5354,-132.3298"/>
|
||||
<text text-anchor="middle" x="562.2959" y="-151.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">invalid user</text>
|
||||
</g>
|
||||
<!-- invite_manager -->
|
||||
<g id="node4" class="node">
|
||||
<title>invite_manager</title>
|
||||
<polygon fill="none" stroke="#ff8c00" points="512.5078,-17 512.5078,-53 612.084,-53 612.084,-17 512.5078,-17"/>
|
||||
<text text-anchor="middle" x="562.2959" y="-30.8" font-family="Times,serif" 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="M274.7912,-80.5452C338.467,-70.4579 438.7527,-54.5711 502.4793,-44.4759"/>
|
||||
<polygon fill="#ff8c00" stroke="#ff8c00" points="503.0528,-47.9288 512.382,-42.9071 501.9575,-41.015 503.0528,-47.9288"/>
|
||||
<text text-anchor="middle" x="455.4386" y="-31.7071" font-family="Times,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="M274.4978,-89.2935C352.2933,-89.0083 492.8294,-90.6942 612.084,-104 628.7169,-105.8558 632.5001,-108.7473 649.084,-111 682.1267,-115.4884 719.327,-118.6586 749.132,-120.7442"/>
|
||||
<polygon fill="#ff0000" stroke="#ff0000" points="749.133,-124.2522 759.347,-121.437 749.6068,-117.2683 749.133,-124.2522"/>
|
||||
<text text-anchor="middle" x="562.2959" y="-106.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">any other user</text>
|
||||
</g>
|
||||
<!-- learn_key -->
|
||||
<g id="node8" class="node">
|
||||
<title>learn_key</title>
|
||||
<polygon fill="none" stroke="#006400" points="771.4272,-17 771.4272,-53 839.1206,-53 839.1206,-17 771.4272,-17"/>
|
||||
<text text-anchor="middle" x="805.2739" y="-30.8" font-family="Times,serif" font-size="14.00" fill="#006400">learn key</text>
|
||||
</g>
|
||||
<!-- invite_manager->learn_key -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>invite_manager->learn_key</title>
|
||||
<path fill="none" stroke="#006400" d="M612.3465,-35C656.1463,-35 719.1598,-35 761.1155,-35"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="761.3041,-38.5001 771.3041,-35 761.304,-31.5001 761.3041,-38.5001"/>
|
||||
<text text-anchor="middle" x="685.8271" y="-37.8" font-family="Times,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="M611.4661,-53.0105C651.6045,-67.7127 708.3017,-88.4802 750.0066,-103.7562"/>
|
||||
<polygon fill="#ff0000" stroke="#ff0000" points="748.8708,-107.0676 759.4646,-107.2206 751.2785,-100.4946 748.8708,-107.0676"/>
|
||||
<text text-anchor="middle" x="685.8271" y="-95.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">invalid token</text>
|
||||
</g>
|
||||
<!-- ssh_proxy -->
|
||||
<g id="node7" class="node">
|
||||
<title>ssh_proxy</title>
|
||||
<polygon fill="none" stroke="#006400" points="766.3516,-168 766.3516,-204 844.1963,-204 844.1963,-168 766.3516,-168"/>
|
||||
<text text-anchor="middle" x="805.2739" y="-181.8" font-family="Times,serif" font-size="14.00" fill="#006400">SSH proxy</text>
|
||||
</g>
|
||||
<!-- acl_manager->ssh_proxy -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>acl_manager->ssh_proxy</title>
|
||||
<path fill="none" stroke="#006400" d="M610.0008,-192.3563C641.8818,-193.0022 684.7518,-193.37 722.5703,-192 733.3636,-191.609 744.9337,-190.9319 755.8983,-190.1699"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="756.4612,-193.6382 766.18,-189.4199 755.9519,-186.6568 756.4612,-193.6382"/>
|
||||
<text text-anchor="middle" x="685.8271" y="-194.8" font-family="Times,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="M610.264,-178.009C646.3866,-168.197 697.1155,-154.3556 741.5703,-142 744.1794,-141.2748 746.8478,-140.5307 749.5426,-139.7772"/>
|
||||
<polygon fill="#ff0000" stroke="#ff0000" points="750.6733,-143.0952 759.3567,-137.025 748.7831,-136.3552 750.6733,-143.0952"/>
|
||||
<text text-anchor="middle" x="685.8271" y="-169.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">unauthorized</text>
|
||||
</g>
|
||||
<!-- host_1 -->
|
||||
<g id="node10" class="node">
|
||||
<title>host_1</title>
|
||||
<polygon fill="none" stroke="#0000ff" points="904.3086,-223 904.3086,-259 986.0908,-259 986.0908,-223 904.3086,-223"/>
|
||||
<text text-anchor="middle" x="945.1997" y="-236.8" font-family="Times,serif" 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="M844.2511,-201.3206C859.7986,-207.4318 877.9046,-214.5486 894.4551,-221.054"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="893.4017,-224.4006 903.9889,-224.8015 895.9624,-217.8858 893.4017,-224.4006"/>
|
||||
</g>
|
||||
<!-- host_2 -->
|
||||
<g id="node11" class="node">
|
||||
<title>host_2</title>
|
||||
<polygon fill="none" stroke="#0000ff" points="887.9775,-168 887.9775,-204 1002.4219,-204 1002.4219,-168 887.9775,-168"/>
|
||||
<text text-anchor="middle" x="945.1997" y="-181.8" font-family="Times,serif" 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="M844.2511,-186C854.6959,-186 866.2954,-186 877.8023,-186"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="877.8592,-189.5001 887.8591,-186 877.8591,-182.5001 877.8592,-189.5001"/>
|
||||
</g>
|
||||
<!-- host_3 -->
|
||||
<g id="node12" class="node">
|
||||
<title>host_3</title>
|
||||
<polygon fill="none" stroke="#0000ff" points="888.3638,-113 888.3638,-149 1002.0356,-149 1002.0356,-113 888.3638,-113"/>
|
||||
<text text-anchor="middle" x="945.1997" y="-126.8" font-family="Times,serif" 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="M844.2511,-170.6794C858.381,-165.1255 874.624,-158.7409 889.8921,-152.7395"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="891.2185,-155.9789 899.245,-149.0632 888.6578,-149.4641 891.2185,-155.9789"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 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];
|
||||
|
||||
}
|
||||
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 |
40
.circleci/config.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
defaults: &defaults
|
||||
working_directory: /go/src/github.com/moul/sshportal
|
||||
docker:
|
||||
- image: circleci/golang:1.8
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
go.build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- run: make install
|
||||
- run: go get -v -t .
|
||||
- run: make test
|
||||
# - run: make integration
|
||||
- run: go get -u github.com/alecthomas/gometalinter
|
||||
- run: gometalinter --install
|
||||
- run: 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.2/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: true
|
||||
- run: docker build -t moul/sshportal .
|
||||
- run: make integration
|
||||
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_integration:
|
||||
jobs:
|
||||
- go.build
|
||||
- docker.integration
|
||||
# requires: docker.build?
|
||||
@@ -1 +1,4 @@
|
||||
examples
|
||||
examples/
|
||||
.circleci/
|
||||
.assets/
|
||||
/sshportal
|
||||
|
||||
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**:
|
||||
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/log/
|
||||
/sshportal
|
||||
*.db
|
||||
/data
|
||||
65
CHANGELOG.md
@@ -1,5 +1,70 @@
|
||||
# Changelog
|
||||
|
||||
## master (unreleased)
|
||||
|
||||
* No entry
|
||||
|
||||
## 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`
|
||||
|
||||
@@ -5,6 +5,9 @@ WORKDIR /go/src/github.com/moul/sshportal
|
||||
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
|
||||
|
||||
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
18
Makefile
@@ -1,12 +1,13 @@
|
||||
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
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
go install -ldflags '$(LDFLAGS)' .
|
||||
go install -v -ldflags '$(LDFLAGS)' .
|
||||
|
||||
.PHONY: docker.build
|
||||
docker.build:
|
||||
@@ -14,7 +15,7 @@ docker.build:
|
||||
|
||||
.PHONY: integration
|
||||
integration:
|
||||
bash ./examples/integration/test.sh
|
||||
cd ./examples/integration && make
|
||||
|
||||
.PHONY: _docker_install
|
||||
_docker_install:
|
||||
@@ -23,14 +24,23 @@ _docker_install:
|
||||
.PHONY: dev
|
||||
dev:
|
||||
-go get github.com/githubnemo/CompileDaemon
|
||||
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal --demo --debug" .
|
||||
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 .
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
gometalinter --disable-all --enable=errcheck --enable=vet --enable=vetshadow --enable=golint --enable=gas --enable=ineffassign --enable=goconst --enable=goimports --enable=gofmt --exclude="should have comment" --enable=staticcheck --enable=gosimple --enable=misspell --deadline=60s .
|
||||
|
||||
.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
|
||||
|
||||
297
README.md
@@ -1,43 +1,70 @@
|
||||
# sshportal
|
||||
|
||||
[](https://circleci.com/gh/moul/sshportal)
|
||||
[](https://hub.docker.com/r/moul/sshportal/)
|
||||
[](https://goreportcard.com/report/github.com/moul/sshportal)
|
||||
[](https://godoc.org/github.com/moul/sshportal)
|
||||
[](https://github.com/moul/sshportal/blob/master/LICENSE)
|
||||
[](https://github.com/moul/sshportal/releases)
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fmoul%2Fsshportal?ref=badge_shield)
|
||||
|
||||
Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
|
||||
|
||||
```
|
||||
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
DMZ │
|
||||
┌────────┐ │ ┌────────┐
|
||||
│ homer │───▶╔═════════════════╗───▶│ host1 │ │
|
||||
└────────┘ ║ ║ └────────┘
|
||||
┌────────┐ ║ ║ ┌────────┐ │
|
||||
│ bart │───▶║ sshportal ║───▶│ host2 │
|
||||
└────────┘ ║ ║ └────────┘ │
|
||||
┌────────┐ ║ ║ ┌────────┐
|
||||
│ lisa │───▶╚═════════════════╝───▶│ host3 │ │
|
||||
└────────┘ │ └────────┘
|
||||
┌────────┐ ┌────────┐ │
|
||||
│ ... │ │ │ ... │
|
||||
└────────┘ └────────┘ │
|
||||
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
```
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
* 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
|
||||
|
||||
## (Known) limitations
|
||||
|
||||
* Does not work (yet?) with [`mosh`](https://mosh.org/)
|
||||
|
||||
## 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 +73,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 +107,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,16 +130,22 @@ 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
|
||||
## Flow Diagram
|
||||
|
||||
sshportal embeds a configuration CLI.
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
@@ -139,50 +162,66 @@ You can enter in interactive mode using this syntax: `ssh admin@portal.example.o
|
||||
```sh
|
||||
# acl management
|
||||
acl help
|
||||
acl create [-h] [--hostgroup=<value>...] [--usergroup=<value>...] [--pattern=<value>] [--comment=<value>] [--action=<value>] [--weight=value]
|
||||
acl inspect [-h] <id>...
|
||||
acl ls [-h]
|
||||
acl rm [-h] <id>...
|
||||
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]
|
||||
config restore [-h] [--confirm]
|
||||
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>] [--fingerprint=<value>] [--comment=<value>] [--key=<value>] [--group=<value>...] <user>[:<password>]@<host>[:<port>]
|
||||
host inspect [-h] <id or name>...
|
||||
host ls [-h]
|
||||
host rm [-h] <id or name>...
|
||||
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] <id or name>...
|
||||
hostgroup ls [-h]
|
||||
hostgroup rm [-h] <id or name>...
|
||||
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 inspect [-h] <id or name>...
|
||||
key ls [-h]
|
||||
key rm [-h] <id or name>...
|
||||
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=<value>...] <email>
|
||||
user inspect [-h] <id or email>...
|
||||
user ls [-h]
|
||||
user rm [-h] <id or email>...
|
||||
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] <id or name>...
|
||||
usergroup ls [-h]
|
||||
usergroup rm [-h] <id or name>...
|
||||
usergroup inspect [-h] USERGROUP...
|
||||
usergroup ls [-h] [--latest] [--quiet]
|
||||
usergroup rm [-h] USERGROUP...
|
||||
|
||||
# other
|
||||
exit [-h]
|
||||
@@ -200,7 +239,7 @@ An [automated build is setup on the Docker Hub](https://hub.docker.com/r/moul/ss
|
||||
```console
|
||||
# Start a server in background
|
||||
# mount `pwd` to persist the sqlite database file
|
||||
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.2.0
|
||||
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.8.0
|
||||
|
||||
# check logs (mandatory on first run to get the administrator invite token)
|
||||
docker logs -f sshportal
|
||||
@@ -209,7 +248,7 @@ docker logs -f sshportal
|
||||
The easier way to upgrade sshportal is to do the following:
|
||||
|
||||
```sh
|
||||
# we consider you were using the version v1.1.0 and you want to use the new version v1.2.0
|
||||
# we consider you were using an old version and you want to use the new version v1.8.0
|
||||
|
||||
# stop and rename the last working container + backup the database
|
||||
docker stop sshportal
|
||||
@@ -217,7 +256,7 @@ 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.2.0
|
||||
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.8.0
|
||||
# check the logs for migration or cross-version incompabitility errors
|
||||
docker logs -f sshportal
|
||||
```
|
||||
@@ -243,16 +282,39 @@ Get the latest version using GO.
|
||||
go get -u github.com/moul/sshportal
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## 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 < sshportal.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).
|
||||
@@ -266,3 +328,90 @@ sqlite3 sshportal.db .dump > sshportal.sql.bkp
|
||||
# or just the immortal cp
|
||||
cp sshportal.db sshportal.db.bkp
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## 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>
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
## Note
|
||||
|
||||
This is totally experimental for now, so please file issues to let me know what you think about it!
|
||||
|
||||
|
||||
## License
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fmoul%2Fsshportal?ref=badge_large) [](https://www.guardrails.io)
|
||||
|
||||
6
acl.go
@@ -26,11 +26,11 @@ func CheckACLs(user User, host Host) (string, error) {
|
||||
|
||||
// deny by default if no shared ACL
|
||||
if len(aclMap) == 0 {
|
||||
return "deny", nil // default action
|
||||
return ACLActionDeny, nil // default action
|
||||
}
|
||||
|
||||
// transofrm map to slice and sort it
|
||||
acls := []*ACL{}
|
||||
// transform map to slice and sort it
|
||||
acls := make([]*ACL, 0, len(aclMap))
|
||||
for _, acl := range aclMap {
|
||||
acls = append(acls, acl)
|
||||
}
|
||||
|
||||
12
acl_test.go
@@ -15,17 +15,21 @@ func TestCheckACLs(t *testing.T) {
|
||||
// create tmp dir
|
||||
tempDir, err := ioutil.TempDir("", "sshportal")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.RemoveAll(tempDir)
|
||||
defer func() {
|
||||
So(os.RemoveAll(tempDir), ShouldBeNil)
|
||||
}()
|
||||
|
||||
// create sqlite db
|
||||
db, err := gorm.Open("sqlite3", filepath.Join(tempDir, "sshportal.db"))
|
||||
So(err, ShouldBeNil)
|
||||
db.LogMode(false)
|
||||
So(dbInit(db), ShouldBeNil)
|
||||
|
||||
// create dummy objects
|
||||
hostGroup, err := FindHostGroupByIdOrName(db, "default")
|
||||
var hostGroup HostGroup
|
||||
err = HostGroupsByIdentifiers(db, []string{"default"}).First(&hostGroup).Error
|
||||
So(err, ShouldBeNil)
|
||||
db.Create(&Host{Groups: []HostGroup{*hostGroup}})
|
||||
db.Create(&Host{Groups: []*HostGroup{&hostGroup}})
|
||||
|
||||
//. load db
|
||||
var (
|
||||
@@ -38,6 +42,6 @@ func TestCheckACLs(t *testing.T) {
|
||||
// test
|
||||
action, err := CheckACLs(users[0], hosts[0])
|
||||
So(err, ShouldBeNil)
|
||||
So(action, ShouldEqual, "allow")
|
||||
So(action, ShouldEqual, ACLActionAllow)
|
||||
})
|
||||
}
|
||||
|
||||
49
config.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type configServe struct {
|
||||
aesKey string
|
||||
dbDriver, dbURL string
|
||||
logsLocation string
|
||||
bindAddr string
|
||||
debug, demo bool
|
||||
}
|
||||
|
||||
func parseServeConfig(c *cli.Context) (*configServe, error) {
|
||||
ret := &configServe{
|
||||
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"),
|
||||
}
|
||||
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
|
||||
}
|
||||
116
crypto.go
@@ -2,11 +2,16 @@ package main
|
||||
|
||||
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"
|
||||
@@ -47,3 +52,114 @@ func NewSSHKey(keyType string, length uint) (*SSHKey, error) {
|
||||
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func ImportSSHKey(keyValue string) (*SSHKey, error) {
|
||||
key := 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 *Host) (err error) {
|
||||
if aesKey == "" {
|
||||
return nil
|
||||
}
|
||||
if host.Password != "" {
|
||||
host.Password, err = encrypt([]byte(aesKey), host.Password)
|
||||
}
|
||||
return
|
||||
}
|
||||
func HostDecrypt(aesKey string, host *Host) {
|
||||
if aesKey == "" {
|
||||
return
|
||||
}
|
||||
if host.Password != "" {
|
||||
host.Password = safeDecrypt([]byte(aesKey), host.Password)
|
||||
}
|
||||
}
|
||||
|
||||
func SSHKeyEncrypt(aesKey string, key *SSHKey) (err error) {
|
||||
if aesKey == "" {
|
||||
return nil
|
||||
}
|
||||
key.PrivKey, err = encrypt([]byte(aesKey), key.PrivKey)
|
||||
return
|
||||
}
|
||||
func SSHKeyDecrypt(aesKey string, key *SSHKey) {
|
||||
if aesKey == "" {
|
||||
return
|
||||
}
|
||||
key.PrivKey = safeDecrypt([]byte(aesKey), key.PrivKey)
|
||||
}
|
||||
|
||||
458
db.go
@@ -1,15 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -21,7 +24,10 @@ type Config struct {
|
||||
HostGroups []*HostGroup `json:"host_groups"`
|
||||
ACLs []*ACL `json:"acls"`
|
||||
Settings []*Setting `json:"settings"`
|
||||
Date time.Time `json:"date"`
|
||||
Events []*Event `json:"events"`
|
||||
Sessions []*Session `json:"sessions"`
|
||||
// FIXME: add latest migration
|
||||
Date time.Time `json:"date"`
|
||||
}
|
||||
|
||||
type Setting struct {
|
||||
@@ -30,6 +36,7 @@ type Setting struct {
|
||||
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
|
||||
@@ -46,29 +53,40 @@ type SSHKey struct {
|
||||
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 *SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Fingerprint string `valid:"optional"` // FIXME: replace with hostKey ?
|
||||
Comment string `valid:"optional"`
|
||||
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
|
||||
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:"required,length(1|10000)"`
|
||||
UserID uint ``
|
||||
User *User `gorm:"ForeignKey:UserID"`
|
||||
Comment string `valid:"optional"`
|
||||
Key []byte `sql:"size:10000" valid:"required,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
|
||||
IsAdmin bool
|
||||
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"`
|
||||
@@ -103,6 +121,52 @@ type ACL struct {
|
||||
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 = "active"
|
||||
SessionStatusClosed = "closed"
|
||||
)
|
||||
|
||||
type ACLAction string
|
||||
|
||||
const (
|
||||
ACLActionUnknown ACLAction = "unknown"
|
||||
ACLActionAllow = "allow"
|
||||
ACLActionDeny = "deny"
|
||||
)
|
||||
|
||||
type BastionScheme string
|
||||
|
||||
const (
|
||||
BastionSchemeSSH BastionScheme = "ssh"
|
||||
BastionSchemeTelnet = "telnet"
|
||||
)
|
||||
|
||||
func init() {
|
||||
unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*")
|
||||
|
||||
@@ -115,188 +179,280 @@ func init() {
|
||||
}))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// Host helpers
|
||||
|
||||
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
|
||||
func ParseInputURL(input string) (*url.URL, error) {
|
||||
if !strings.Contains(input, "://") {
|
||||
input = "ssh://" + input
|
||||
}
|
||||
u, err := url.Parse(rawurl)
|
||||
u, err := url.Parse(input)
|
||||
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
|
||||
return u, nil
|
||||
}
|
||||
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 {
|
||||
return strings.Split(host.Addr, ":")[0]
|
||||
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 ""
|
||||
}
|
||||
|
||||
// Host helpers
|
||||
|
||||
func FindHostByIdOrName(db *gorm.DB, query string) (*Host, error) {
|
||||
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
|
||||
if err := db.Preload("Groups").Preload("SSHKey").Where("id = ?", query).Or("name = ?", query).First(&host).Error; err != nil {
|
||||
return nil, err
|
||||
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 FindHostsByIdOrName(db *gorm.DB, queries []string) ([]*Host, error) {
|
||||
var hosts []*Host
|
||||
for _, query := range queries {
|
||||
host, err := FindHostByIdOrName(db, query)
|
||||
|
||||
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
|
||||
}
|
||||
hosts = append(hosts, host)
|
||||
config.Auth = append(config.Auth, gossh.PublicKeys(signer))
|
||||
}
|
||||
return hosts, nil
|
||||
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 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 SSHKeysPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Hosts")
|
||||
}
|
||||
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
|
||||
func SSHKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// 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 HostGroupsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ACLs").Preload("Hosts")
|
||||
}
|
||||
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
|
||||
func HostGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// UserGroup heleprs
|
||||
// UserGroup helpers
|
||||
|
||||
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 UserGroupsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ACLs").Preload("Users")
|
||||
}
|
||||
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
|
||||
func UserGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// 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 UsersPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Groups").Preload("Keys").Preload("Roles")
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
return users, nil
|
||||
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 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 ACLsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("UserGroups").Preload("HostGroups")
|
||||
}
|
||||
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
|
||||
func ACLsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// 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 UserKeysPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("User")
|
||||
}
|
||||
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
|
||||
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)
|
||||
}
|
||||
userkeys = append(userkeys, userkey)
|
||||
}
|
||||
return userkeys, nil
|
||||
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
|
||||
}
|
||||
|
||||
380
dbinit.go
@@ -1,14 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate"
|
||||
"github.com/jinzhu/gorm"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
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",
|
||||
@@ -23,8 +33,7 @@ func dbInit(db *gorm.DB) error {
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("settings").Error
|
||||
},
|
||||
},
|
||||
{
|
||||
}, {
|
||||
ID: "2",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type SSHKey struct {
|
||||
@@ -151,7 +160,7 @@ func dbInit(db *gorm.DB) error {
|
||||
ID: "9",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&Setting{}).RemoveIndex("uix_settings_name")
|
||||
return db.Model(&Setting{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_settings_name", "name").Error
|
||||
return db.Model(&Setting{}).AddUniqueIndex("uix_settings_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&Setting{}).RemoveIndex("uix_settings_name").Error
|
||||
@@ -160,7 +169,7 @@ func dbInit(db *gorm.DB) error {
|
||||
ID: "10",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&SSHKey{}).RemoveIndex("uix_keys_name")
|
||||
return db.Model(&SSHKey{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_keys_name", "name").Error
|
||||
return db.Model(&SSHKey{}).AddUniqueIndex("uix_keys_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&SSHKey{}).RemoveIndex("uix_keys_name").Error
|
||||
@@ -169,7 +178,7 @@ func dbInit(db *gorm.DB) error {
|
||||
ID: "11",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&Host{}).RemoveIndex("uix_hosts_name")
|
||||
return db.Model(&Host{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_hosts_name", "name").Error
|
||||
return db.Model(&Host{}).AddUniqueIndex("uix_hosts_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&Host{}).RemoveIndex("uix_hosts_name").Error
|
||||
@@ -178,7 +187,7 @@ func dbInit(db *gorm.DB) error {
|
||||
ID: "12",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&User{}).RemoveIndex("uix_users_name")
|
||||
return db.Model(&User{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_users_name", "name").Error
|
||||
return db.Model(&User{}).AddUniqueIndex("uix_users_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&User{}).RemoveIndex("uix_users_name").Error
|
||||
@@ -187,7 +196,7 @@ func dbInit(db *gorm.DB) error {
|
||||
ID: "13",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&UserGroup{}).RemoveIndex("uix_usergroups_name")
|
||||
return db.Model(&UserGroup{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_usergroups_name", "name").Error
|
||||
return db.Model(&UserGroup{}).AddUniqueIndex("uix_usergroups_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&UserGroup{}).RemoveIndex("uix_usergroups_name").Error
|
||||
@@ -196,16 +205,289 @@ func dbInit(db *gorm.DB) error {
|
||||
ID: "14",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&HostGroup{}).RemoveIndex("uix_hostgroups_name")
|
||||
return db.Model(&HostGroup{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_hostgroups_name", "name").Error
|
||||
return db.Model(&HostGroup{}).AddUniqueIndex("uix_hostgroups_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&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 []*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 []*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)"`
|
||||
}
|
||||
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(&UserRole{Name: "admin"}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Where("name = ?", "admin").Delete(&UserRole{}).Error
|
||||
},
|
||||
}, {
|
||||
ID: "18",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var adminRole UserRole
|
||||
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var users []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 []*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)"`
|
||||
}
|
||||
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(&UserRole{Name: "listhosts"}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Where("name = ?", "listhosts").Delete(&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 *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"`
|
||||
}
|
||||
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 *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 *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 []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 *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;"`
|
||||
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 *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"`
|
||||
}
|
||||
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 []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 *SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000"`
|
||||
Groups []*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 *SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string
|
||||
Hop *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
|
||||
}
|
||||
NewEvent("system", "migrated").Log(db)
|
||||
|
||||
// create default ssh key
|
||||
var count uint
|
||||
@@ -277,23 +559,42 @@ func dbInit(db *gorm.DB) error {
|
||||
// create admin user
|
||||
var defaultUserGroup UserGroup
|
||||
db.Where("name = ?", "default").First(&defaultUserGroup)
|
||||
db.Table("users").Count(&count)
|
||||
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)
|
||||
inviteToken := randStringBytes(16)
|
||||
if os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN") != "" {
|
||||
inviteToken = os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN")
|
||||
}
|
||||
var adminRole 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 := User{
|
||||
Name: "Administrator",
|
||||
Email: "admin@sshportal",
|
||||
Name: username,
|
||||
Email: fmt.Sprintf("%s@localhost", username),
|
||||
Comment: "created by sshportal",
|
||||
IsAdmin: true,
|
||||
Roles: []*UserRole{&adminRole},
|
||||
InviteToken: inviteToken,
|
||||
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)
|
||||
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
|
||||
@@ -311,29 +612,34 @@ func dbInit(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
// close unclosed connections
|
||||
return db.Table("sessions").Where("status = ?", "active").Updates(&Session{
|
||||
Status: SessionStatusClosed,
|
||||
ErrMsg: "sshportal was halted while the connection was still active",
|
||||
}).Error
|
||||
}
|
||||
|
||||
func dbDemo(db *gorm.DB) error {
|
||||
hostGroup, err := FindHostGroupByIdOrName(db, "default")
|
||||
if err != nil {
|
||||
return err
|
||||
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()
|
||||
}
|
||||
|
||||
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.FirstOrCreate(&host1)
|
||||
db.FirstOrCreate(&host2)
|
||||
db.FirstOrCreate(&host3)
|
||||
return nil
|
||||
}
|
||||
|
||||
func addExtraSpaceIfExist(str string) string {
|
||||
if str != "" {
|
||||
return " " + str
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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
|
||||
7
examples/integration/Makefile
Normal file
@@ -0,0 +1,7 @@
|
||||
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
|
||||
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 usergroup 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
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/bin/sh -e
|
||||
# Setup a new sshportal and performs some checks
|
||||
|
||||
PORT=${PORT:-2222}
|
||||
SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN=integration
|
||||
|
||||
# tempdir
|
||||
WORK_DIR=`mktemp -d`
|
||||
if [[ ! "$WORK_DIR" || ! -d "$WORK_DIR" ]]; then
|
||||
echo "Could not create temp dir"
|
||||
exit 1
|
||||
fi
|
||||
cd "${WORK_DIR}"
|
||||
|
||||
# pre cleanup
|
||||
docker_cleanup() {
|
||||
( set -x
|
||||
docker rm -f -v sshportal-integration 2>/dev/null >/dev/null || true
|
||||
)
|
||||
}
|
||||
tempdir_cleanup() {
|
||||
rm -rf "${WORK_DIR}"
|
||||
}
|
||||
docker_cleanup
|
||||
trap tempdir_cleanup EXIT
|
||||
|
||||
# start server
|
||||
( set -xe;
|
||||
docker run \
|
||||
-d \
|
||||
-e SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN=${SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN} \
|
||||
--name=sshportal-integration \
|
||||
-p${PORT}:2222 \
|
||||
moul/sshportal --debug
|
||||
)
|
||||
while ! nc -z localhost ${PORT}; do
|
||||
sleep 1
|
||||
done
|
||||
sleep 3
|
||||
|
||||
# integration suite
|
||||
xssh() {
|
||||
set -e
|
||||
echo "+ ssh {sshportal} $@" >&2
|
||||
ssh -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no localhost -p ${PORT} $@
|
||||
}
|
||||
# login
|
||||
xssh -l invite:integration
|
||||
|
||||
# hostgroup/usergroup/acl
|
||||
xssh -l admin hostgroup create
|
||||
xssh -l admin hostgroup create --name=hg1
|
||||
xssh -l admin hostgroup create --name=hg2 --comment=test
|
||||
xssh -l admin usergroup inspect hg1 hg2
|
||||
xssh -l admin hostgroup ls
|
||||
|
||||
xssh -l admin usergroup create
|
||||
xssh -l admin usergroup create --name=ug1
|
||||
xssh -l admin usergroup create --name=ug2 --comment=test
|
||||
xssh -l admin usergroup inspect ug1 ug2
|
||||
xssh -l admin usergroup ls
|
||||
|
||||
xssh -l admin acl create --ug=ug1 --ug=ug2 --hg=hg1 --hg=hg2 --comment=test --action=allow --weight=42
|
||||
xssh -l admin acl inspect 2
|
||||
xssh -l admin acl ls
|
||||
|
||||
# basic host create
|
||||
xssh -l admin host create bob@example.org:1234
|
||||
xssh -l admin host create test42
|
||||
xssh -l admin host create --name=testtest --comment=test --password=test test@test.test
|
||||
xssh -l admin host create --group=hg1 --group=hg2 hostwithgroups.org
|
||||
xssh -l admin host inspect example test42 testtest hostwithgroups
|
||||
xssh -l admin host ls
|
||||
|
||||
# backup/restore
|
||||
xssh -l admin config backup --indent > backup-1
|
||||
xssh -l admin config restore --confirm < backup-1
|
||||
xssh -l admin config backup --indent > backup-2
|
||||
diff <(cat backup-1 | grep -v '"date":') <(cat backup-2 | grep -v '"date":')
|
||||
|
||||
# post cleanup
|
||||
#cleanup
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
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 --db-driver=mysql --debug --db-conn="root:root@tcp(mysql:3306)/db?charset=utf8&parseTime=true&loc=Local"
|
||||
ports:
|
||||
- 2222:2222
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
78
hidden.go
Normal file
@@ -0,0 +1,78 @@
|
||||
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)
|
||||
}
|
||||
316
main.go
@@ -1,13 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
@@ -15,26 +14,17 @@ import (
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/urfave/cli"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
// VERSION should be updated by hand at each release
|
||||
VERSION = "1.2.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.8.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() {
|
||||
@@ -43,198 +33,130 @@ func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = path.Base(os.Args[0])
|
||||
app.Author = "Manfred Touron"
|
||||
app.Version = VERSION + " (" + GIT_SHA + ")"
|
||||
app.Version = Version + " (" + GitSha + ")"
|
||||
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.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 := parseServeConfig(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",
|
||||
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: "aes-key",
|
||||
Usage: "Encrypt sensitive data in database (length: 16, 24 or 32)",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "logs-location",
|
||||
Value: "./log",
|
||||
Usage: "Store user session files",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
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
|
||||
db, err := gorm.Open("sqlite3", c.String("db-conn"))
|
||||
func server(c *configServe) (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 = dbInit(db); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// create TCP listening socket
|
||||
ln, err := net.Listen("tcp", c.bindAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
if err = db.DB().Ping(); err != nil {
|
||||
return err
|
||||
|
||||
// configure server
|
||||
srv := &ssh.Server{
|
||||
Addr: c.bindAddr,
|
||||
Handler: shellHandler, // ssh.Server.Handler is the handler for the DefaultSessionHandler
|
||||
Version: fmt.Sprintf("sshportal-%s", Version),
|
||||
ChannelHandler: channelHandler,
|
||||
}
|
||||
if c.Bool("debug") {
|
||||
db.LogMode(true)
|
||||
}
|
||||
if err := dbInit(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Bool("demo") {
|
||||
if err := dbDemo(db); err != nil {
|
||||
|
||||
for _, opt := range []ssh.Option{
|
||||
// custom PublicKeyAuth handler
|
||||
ssh.PublicKeyAuth(publicKeyAuthHandler(db, c)),
|
||||
ssh.PasswordAuth(passwordAuthHandler(db, c)),
|
||||
// retrieve sshportal SSH private key from database
|
||||
privateKeyFromDB(db, c.aesKey),
|
||||
} {
|
||||
if err := srv.SetOption(opt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// ssh server
|
||||
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{}
|
||||
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) > 0 {
|
||||
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...)
|
||||
log.Printf("info: SSH Server accepting connections on %s", c.bindAddr)
|
||||
return srv.Serve(ln)
|
||||
}
|
||||
|
||||
201
pkg/bastionsession/bastionsession.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package bastionsession
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/arkan/bastion/pkg/logchannel"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/moul/sshportal/pkg/logtunnel"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type ForwardData struct {
|
||||
DestinationHost string
|
||||
DestinationPort uint32
|
||||
SourceHost string
|
||||
SourcePort uint32
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Addr string
|
||||
Logs string
|
||||
ClientConfig *gossh.ClientConfig
|
||||
}
|
||||
|
||||
func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []Config) 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 {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
lastClient = client
|
||||
}
|
||||
|
||||
rch, rreqs, err := lastClient.OpenChannel("session", []byte{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := conn.User()
|
||||
// pipe everything
|
||||
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, 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 {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
lastClient = client
|
||||
}
|
||||
|
||||
d := logtunnel.ForwardData{}
|
||||
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()
|
||||
// pipe everything
|
||||
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, newChan)
|
||||
default:
|
||||
newChan.Reject(gossh.UnknownChannelType, "unsupported channel type")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocation string, user string, newChan gossh.NewChannel) error {
|
||||
defer func() {
|
||||
_ = lch.Close()
|
||||
_ = rch.Close()
|
||||
}()
|
||||
|
||||
errch := make(chan error, 1)
|
||||
channeltype := newChan.ChannelType()
|
||||
|
||||
file_name := strings.Join([]string{logsLocation, "/", user, "-", channeltype, "-", time.Now().Format(time.RFC3339)}, "") // get user
|
||||
f, err := os.OpenFile(file_name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640)
|
||||
defer f.Close()
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("error: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Session %v is recorded in %v", channeltype, file_name)
|
||||
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 := logtunnel.ForwardData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
return err
|
||||
}
|
||||
wrappedlch := logtunnel.New(lch, f, d.SourceHost)
|
||||
wrappedrch := logtunnel.New(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")...)
|
||||
wrappedlch.LogWrite(command)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
59
pkg/logtunnel/logtunnel.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package logtunnel
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type logTunnel struct {
|
||||
host string
|
||||
channel ssh.Channel
|
||||
writer io.WriteCloser
|
||||
}
|
||||
|
||||
type ForwardData struct {
|
||||
DestinationHost string
|
||||
DestinationPort uint32
|
||||
SourceHost string
|
||||
SourcePort uint32
|
||||
}
|
||||
|
||||
func writeHeader(fd io.Writer, length int) {
|
||||
t := time.Now()
|
||||
|
||||
tv := syscall.NsecToTimeval(t.UnixNano())
|
||||
|
||||
binary.Write(fd, binary.LittleEndian, int32(tv.Sec))
|
||||
binary.Write(fd, binary.LittleEndian, int32(tv.Usec))
|
||||
binary.Write(fd, binary.LittleEndian, int32(length))
|
||||
}
|
||||
|
||||
func New(channel ssh.Channel, writer io.WriteCloser, host string) *logTunnel {
|
||||
return &logTunnel{
|
||||
host: host,
|
||||
channel: channel,
|
||||
writer: writer,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logTunnel) Read(data []byte) (int, error) {
|
||||
return l.Read(data)
|
||||
}
|
||||
|
||||
func (l *logTunnel) Write(data []byte) (int, error) {
|
||||
writeHeader(l.writer, len(data) + len(l.host + ": "))
|
||||
l.writer.Write([]byte(l.host + ": "))
|
||||
l.writer.Write(data)
|
||||
|
||||
return l.channel.Write(data)
|
||||
}
|
||||
|
||||
func (l *logTunnel) Close() error {
|
||||
l.writer.Close()
|
||||
|
||||
return l.channel.Close()
|
||||
}
|
||||
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
|
||||
}
|
||||
350
ssh.go
Normal file
@@ -0,0 +1,350 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/moul/sshportal/pkg/bastionsession"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type sshportalContextKey string
|
||||
|
||||
var authContextKey = sshportalContextKey("auth")
|
||||
|
||||
type authContext struct {
|
||||
message string
|
||||
err error
|
||||
user User
|
||||
inputUsername string
|
||||
db *gorm.DB
|
||||
userKey UserKey
|
||||
config *configServe
|
||||
authMethod string
|
||||
authSuccess bool
|
||||
}
|
||||
|
||||
type UserType string
|
||||
|
||||
const (
|
||||
UserTypeHealthcheck UserType = "healthcheck"
|
||||
UserTypeBastion = "bastion"
|
||||
UserTypeInvite = "invite"
|
||||
UserTypeShell = "shell"
|
||||
)
|
||||
|
||||
type SessionType string
|
||||
|
||||
const (
|
||||
SessionTypeBastion SessionType = "bastion"
|
||||
SessionTypeShell = "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 (c authContext) sessionType() SessionType {
|
||||
switch c.userType() {
|
||||
case "bastion":
|
||||
return SessionTypeBastion
|
||||
default:
|
||||
return SessionTypeShell
|
||||
}
|
||||
}
|
||||
|
||||
func dynamicHostKey(db *gorm.DB, host *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
|
||||
}
|
||||
}
|
||||
|
||||
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:%q,email:%s", conn.User(), conn.RemoteAddr(), conn.LocalAddr(), actx.user.ID, actx.user.Email)
|
||||
host, err := 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 BastionSchemeSSH:
|
||||
sessionConfigs := make([]bastionsession.Config, 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([]bastionsession.Config{{
|
||||
Addr: currentHost.DialAddr(),
|
||||
ClientConfig: clientConfig,
|
||||
Logs: actx.config.logsLocation,
|
||||
}}, sessionConfigs...)
|
||||
if currentHost.HopID != 0 {
|
||||
var newHost Host
|
||||
actx.db.Model(currentHost).Related(&newHost, "HopID")
|
||||
hostname := newHost.Name
|
||||
currentHost, _ = HostByName(actx.db, hostname)
|
||||
} else {
|
||||
currentHost = nil
|
||||
}
|
||||
}
|
||||
|
||||
sess := Session{
|
||||
UserID: actx.user.ID,
|
||||
HostID: host.ID,
|
||||
Status: 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 = bastionsession.MultiChannelHandler(srv, conn, newChan, ctx, sessionConfigs)
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
now := time.Now()
|
||||
sessUpdate := Session{
|
||||
Status: 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 BastionSchemeTelnet:
|
||||
tmpSrv := ssh.Server{
|
||||
// PtyCallback: srv.PtyCallback,
|
||||
Handler: telnetHandler(host),
|
||||
}
|
||||
ssh.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
|
||||
ssh.DefaultChannelHandler(srv, conn, newChan, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func bastionClientConfig(ctx ssh.Context, host *Host) (*gossh.ClientConfig, error) {
|
||||
actx := ctx.Value(authContextKey).(*authContext)
|
||||
|
||||
clientConfig, err := host.clientConfig(dynamicHostKey(actx.db, host))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tmpUser 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 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
|
||||
}
|
||||
|
||||
HostDecrypt(actx.config.aesKey, host)
|
||||
SSHKeyDecrypt(actx.config.aesKey, host.SSHKey)
|
||||
|
||||
switch action {
|
||||
case ACLActionAllow:
|
||||
case 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) {
|
||||
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:%q,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); 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, cfg *configServe) ssh.PasswordHandler {
|
||||
return func(ctx ssh.Context, pass string) bool {
|
||||
actx := &authContext{
|
||||
db: db,
|
||||
inputUsername: ctx.User(),
|
||||
config: cfg,
|
||||
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 SSHKey
|
||||
if err := SSHKeysByIdentifiers(db, []string{"host"}).First(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
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, cfg *configServe) ssh.PublicKeyHandler {
|
||||
return func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
actx := &authContext{
|
||||
db: db,
|
||||
inputUsername: ctx.User(),
|
||||
config: cfg,
|
||||
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 = 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 = 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 = User{Name: "Anonymous"}
|
||||
return true
|
||||
}
|
||||
}
|
||||
87
telnet.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
oi "github.com/reiver/go-oi"
|
||||
telnet "github.com/reiver/go-telnet"
|
||||
)
|
||||
|
||||
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 *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)
|
||||
}
|
||||
}
|
||||
}
|
||||
9
util.go
@@ -4,10 +4,17 @@ import "math/rand"
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
func RandStringBytes(n int) string {
|
||||
func randStringBytes(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func wrapText(in string, length int) string {
|
||||
if len(in) <= length {
|
||||
return in
|
||||
}
|
||||
return in[0:length-3] + "..."
|
||||
}
|
||||
|
||||
22
vendor/github.com/arkan/bastion/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-2017 Florian Bertholin
|
||||
|
||||
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.
|
||||
|
||||
54
vendor/github.com/arkan/bastion/pkg/logchannel/logchannel.go
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
package logchannel
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type logChannel struct {
|
||||
channel ssh.Channel
|
||||
writer io.WriteCloser
|
||||
}
|
||||
|
||||
func writeTTYRecHeader(fd io.Writer, length int) {
|
||||
t := time.Now()
|
||||
|
||||
tv := syscall.NsecToTimeval(t.UnixNano())
|
||||
|
||||
binary.Write(fd, binary.LittleEndian, int32(tv.Sec))
|
||||
binary.Write(fd, binary.LittleEndian, int32(tv.Usec))
|
||||
binary.Write(fd, binary.LittleEndian, int32(length))
|
||||
}
|
||||
|
||||
func New(channel ssh.Channel, writer io.WriteCloser) *logChannel {
|
||||
return &logChannel{
|
||||
channel: channel,
|
||||
writer: writer,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logChannel) Read(data []byte) (int, error) {
|
||||
return l.Read(data)
|
||||
}
|
||||
|
||||
func (l *logChannel) Write(data []byte) (int, error) {
|
||||
writeTTYRecHeader(l.writer, len(data))
|
||||
l.writer.Write(data)
|
||||
|
||||
return l.channel.Write(data)
|
||||
}
|
||||
|
||||
func (l *logChannel) LogWrite(data []byte) (int, error) {
|
||||
writeTTYRecHeader(l.writer, len(data))
|
||||
return l.writer.Write(data)
|
||||
}
|
||||
|
||||
func (l *logChannel) Close() error {
|
||||
l.writer.Close()
|
||||
|
||||
return l.channel.Close()
|
||||
}
|
||||
21
vendor/github.com/dustin/go-humanize/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
|
||||
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.
|
||||
|
||||
<http://www.opensource.org/licenses/mit-license.php>
|
||||
92
vendor/github.com/dustin/go-humanize/README.markdown
generated
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
# Humane Units [](https://travis-ci.org/dustin/go-humanize) [](https://godoc.org/github.com/dustin/go-humanize)
|
||||
|
||||
Just a few functions for helping humanize times and sizes.
|
||||
|
||||
`go get` it as `github.com/dustin/go-humanize`, import it as
|
||||
`"github.com/dustin/go-humanize"`, use it as `humanize`
|
||||
|
||||
See [godoc](https://godoc.org/github.com/dustin/go-humanize) for
|
||||
complete documentation.
|
||||
|
||||
## Sizes
|
||||
|
||||
This lets you take numbers like `82854982` and convert them to useful
|
||||
strings like, `83MB` or `79MiB` (whichever you prefer).
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("That file is %s.", humanize.Bytes(82854982))
|
||||
```
|
||||
|
||||
## Times
|
||||
|
||||
This lets you take a `time.Time` and spit it out in relative terms.
|
||||
For example, `12 seconds ago` or `3 days from now`.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("This was touched %s", humanize.Time(someTimeInstance))
|
||||
```
|
||||
|
||||
Thanks to Kyle Lemons for the time implementation from an IRC
|
||||
conversation one day. It's pretty neat.
|
||||
|
||||
## Ordinals
|
||||
|
||||
From a [mailing list discussion][odisc] where a user wanted to be able
|
||||
to label ordinals.
|
||||
|
||||
0 -> 0th
|
||||
1 -> 1st
|
||||
2 -> 2nd
|
||||
3 -> 3rd
|
||||
4 -> 4th
|
||||
[...]
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193))
|
||||
```
|
||||
|
||||
## Commas
|
||||
|
||||
Want to shove commas into numbers? Be my guest.
|
||||
|
||||
0 -> 0
|
||||
100 -> 100
|
||||
1000 -> 1,000
|
||||
1000000000 -> 1,000,000,000
|
||||
-100000 -> -100,000
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491))
|
||||
```
|
||||
|
||||
## Ftoa
|
||||
|
||||
Nicer float64 formatter that removes trailing zeros.
|
||||
|
||||
```go
|
||||
fmt.Printf("%f", 2.24) // 2.240000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
|
||||
fmt.Printf("%f", 2.0) // 2.000000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
|
||||
```
|
||||
|
||||
## SI notation
|
||||
|
||||
Format numbers with [SI notation][sinotation].
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
humanize.SI(0.00000000223, "M") // 2.23nM
|
||||
```
|
||||
|
||||
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
|
||||
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix
|
||||
31
vendor/github.com/dustin/go-humanize/big.go
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// order of magnitude (to a max order)
|
||||
func oomm(n, b *big.Int, maxmag int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
if mag == maxmag && maxmag >= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
||||
|
||||
// total order of magnitude
|
||||
// (same as above, but with no upper limit)
|
||||
func oom(n, b *big.Int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
||||
173
vendor/github.com/dustin/go-humanize/bigbytes.go
generated
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
bigIECExp = big.NewInt(1024)
|
||||
|
||||
// BigByte is one byte in bit.Ints
|
||||
BigByte = big.NewInt(1)
|
||||
// BigKiByte is 1,024 bytes in bit.Ints
|
||||
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp)
|
||||
// BigMiByte is 1,024 k bytes in bit.Ints
|
||||
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp)
|
||||
// BigGiByte is 1,024 m bytes in bit.Ints
|
||||
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp)
|
||||
// BigTiByte is 1,024 g bytes in bit.Ints
|
||||
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp)
|
||||
// BigPiByte is 1,024 t bytes in bit.Ints
|
||||
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp)
|
||||
// BigEiByte is 1,024 p bytes in bit.Ints
|
||||
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp)
|
||||
// BigZiByte is 1,024 e bytes in bit.Ints
|
||||
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp)
|
||||
// BigYiByte is 1,024 z bytes in bit.Ints
|
||||
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp)
|
||||
)
|
||||
|
||||
var (
|
||||
bigSIExp = big.NewInt(1000)
|
||||
|
||||
// BigSIByte is one SI byte in big.Ints
|
||||
BigSIByte = big.NewInt(1)
|
||||
// BigKByte is 1,000 SI bytes in big.Ints
|
||||
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp)
|
||||
// BigMByte is 1,000 SI k bytes in big.Ints
|
||||
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp)
|
||||
// BigGByte is 1,000 SI m bytes in big.Ints
|
||||
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp)
|
||||
// BigTByte is 1,000 SI g bytes in big.Ints
|
||||
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp)
|
||||
// BigPByte is 1,000 SI t bytes in big.Ints
|
||||
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp)
|
||||
// BigEByte is 1,000 SI p bytes in big.Ints
|
||||
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp)
|
||||
// BigZByte is 1,000 SI e bytes in big.Ints
|
||||
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp)
|
||||
// BigYByte is 1,000 SI z bytes in big.Ints
|
||||
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp)
|
||||
)
|
||||
|
||||
var bigBytesSizeTable = map[string]*big.Int{
|
||||
"b": BigByte,
|
||||
"kib": BigKiByte,
|
||||
"kb": BigKByte,
|
||||
"mib": BigMiByte,
|
||||
"mb": BigMByte,
|
||||
"gib": BigGiByte,
|
||||
"gb": BigGByte,
|
||||
"tib": BigTiByte,
|
||||
"tb": BigTByte,
|
||||
"pib": BigPiByte,
|
||||
"pb": BigPByte,
|
||||
"eib": BigEiByte,
|
||||
"eb": BigEByte,
|
||||
"zib": BigZiByte,
|
||||
"zb": BigZByte,
|
||||
"yib": BigYiByte,
|
||||
"yb": BigYByte,
|
||||
// Without suffix
|
||||
"": BigByte,
|
||||
"ki": BigKiByte,
|
||||
"k": BigKByte,
|
||||
"mi": BigMiByte,
|
||||
"m": BigMByte,
|
||||
"gi": BigGiByte,
|
||||
"g": BigGByte,
|
||||
"ti": BigTiByte,
|
||||
"t": BigTByte,
|
||||
"pi": BigPiByte,
|
||||
"p": BigPByte,
|
||||
"ei": BigEiByte,
|
||||
"e": BigEByte,
|
||||
"z": BigZByte,
|
||||
"zi": BigZiByte,
|
||||
"y": BigYByte,
|
||||
"yi": BigYiByte,
|
||||
}
|
||||
|
||||
var ten = big.NewInt(10)
|
||||
|
||||
func humanateBigBytes(s, base *big.Int, sizes []string) string {
|
||||
if s.Cmp(ten) < 0 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
c := (&big.Int{}).Set(s)
|
||||
val, mag := oomm(c, base, len(sizes)-1)
|
||||
suffix := sizes[mag]
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
|
||||
}
|
||||
|
||||
// BigBytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigBytes(82854982) -> 83MB
|
||||
func BigBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
||||
return humanateBigBytes(s, bigSIExp, sizes)
|
||||
}
|
||||
|
||||
// BigIBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigIBytes(82854982) -> 79MiB
|
||||
func BigIBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
|
||||
return humanateBigBytes(s, bigIECExp, sizes)
|
||||
}
|
||||
|
||||
// ParseBigBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See also: BigBytes, BigIBytes.
|
||||
//
|
||||
// ParseBigBytes("42MB") -> 42000000, nil
|
||||
// ParseBigBytes("42mib") -> 44040192, nil
|
||||
func ParseBigBytes(s string) (*big.Int, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
val := &big.Rat{}
|
||||
_, err := fmt.Sscanf(num, "%f", val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bigBytesSizeTable[extra]; ok {
|
||||
mv := (&big.Rat{}).SetInt(m)
|
||||
val.Mul(val, mv)
|
||||
rv := &big.Int{}
|
||||
rv.Div(val.Num(), val.Denom())
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
||||
143
vendor/github.com/dustin/go-humanize/bytes.go
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// IEC Sizes.
|
||||
// kibis of bits
|
||||
const (
|
||||
Byte = 1 << (iota * 10)
|
||||
KiByte
|
||||
MiByte
|
||||
GiByte
|
||||
TiByte
|
||||
PiByte
|
||||
EiByte
|
||||
)
|
||||
|
||||
// SI Sizes.
|
||||
const (
|
||||
IByte = 1
|
||||
KByte = IByte * 1000
|
||||
MByte = KByte * 1000
|
||||
GByte = MByte * 1000
|
||||
TByte = GByte * 1000
|
||||
PByte = TByte * 1000
|
||||
EByte = PByte * 1000
|
||||
)
|
||||
|
||||
var bytesSizeTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"kib": KiByte,
|
||||
"kb": KByte,
|
||||
"mib": MiByte,
|
||||
"mb": MByte,
|
||||
"gib": GiByte,
|
||||
"gb": GByte,
|
||||
"tib": TiByte,
|
||||
"tb": TByte,
|
||||
"pib": PiByte,
|
||||
"pb": PByte,
|
||||
"eib": EiByte,
|
||||
"eb": EByte,
|
||||
// Without suffix
|
||||
"": Byte,
|
||||
"ki": KiByte,
|
||||
"k": KByte,
|
||||
"mi": MiByte,
|
||||
"m": MByte,
|
||||
"gi": GiByte,
|
||||
"g": GByte,
|
||||
"ti": TiByte,
|
||||
"t": TByte,
|
||||
"pi": PiByte,
|
||||
"p": PByte,
|
||||
"ei": EiByte,
|
||||
"e": EByte,
|
||||
}
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
|
||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
}
|
||||
|
||||
// Bytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// Bytes(82854982) -> 83MB
|
||||
func Bytes(s uint64) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||
return humanateBytes(s, 1000, sizes)
|
||||
}
|
||||
|
||||
// IBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// IBytes(82854982) -> 79MiB
|
||||
func IBytes(s uint64) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
return humanateBytes(s, 1024, sizes)
|
||||
}
|
||||
|
||||
// ParseBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See Also: Bytes, IBytes.
|
||||
//
|
||||
// ParseBytes("42MB") -> 42000000, nil
|
||||
// ParseBytes("42mib") -> 44040192, nil
|
||||
func ParseBytes(s string) (uint64, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(num, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bytesSizeTable[extra]; ok {
|
||||
f *= float64(m)
|
||||
if f >= math.MaxUint64 {
|
||||
return 0, fmt.Errorf("too large: %v", s)
|
||||
}
|
||||
return uint64(f), nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
||||
108
vendor/github.com/dustin/go-humanize/comma.go
generated
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Comma produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Comma(834142) -> 834,142
|
||||
func Comma(v int64) string {
|
||||
sign := ""
|
||||
|
||||
// minin64 can't be negated to a usable value, so it has to be special cased.
|
||||
if v == math.MinInt64 {
|
||||
return "-9,223,372,036,854,775,808"
|
||||
}
|
||||
|
||||
if v < 0 {
|
||||
sign = "-"
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
parts := []string{"", "", "", "", "", "", ""}
|
||||
j := len(parts) - 1
|
||||
|
||||
for v > 999 {
|
||||
parts[j] = strconv.FormatInt(v%1000, 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
v = v / 1000
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(v))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
||||
|
||||
// Commaf produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Commaf(834142.32) -> 834,142.32
|
||||
func Commaf(v float64) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// BigComma produces a string form of the given big.Int in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigComma(b *big.Int) string {
|
||||
sign := ""
|
||||
if b.Sign() < 0 {
|
||||
sign = "-"
|
||||
b.Abs(b)
|
||||
}
|
||||
|
||||
athousand := big.NewInt(1000)
|
||||
c := (&big.Int{}).Set(b)
|
||||
_, m := oom(c, athousand)
|
||||
parts := make([]string, m+1)
|
||||
j := len(parts) - 1
|
||||
|
||||
mod := &big.Int{}
|
||||
for b.Cmp(athousand) >= 0 {
|
||||
b.DivMod(b, athousand, mod)
|
||||
parts[j] = strconv.FormatInt(mod.Int64(), 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(b.Int64()))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
||||
40
vendor/github.com/dustin/go-humanize/commaf.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
// +build go1.6
|
||||
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BigCommaf produces a string form of the given big.Float in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigCommaf(v *big.Float) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v.Sign() < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v.Abs(v)
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(v.Text('f', -1), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
23
vendor/github.com/dustin/go-humanize/ftoa.go
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
package humanize
|
||||
|
||||
import "strconv"
|
||||
|
||||
func stripTrailingZeros(s string) string {
|
||||
offset := len(s) - 1
|
||||
for offset > 0 {
|
||||
if s[offset] == '.' {
|
||||
offset--
|
||||
break
|
||||
}
|
||||
if s[offset] != '0' {
|
||||
break
|
||||
}
|
||||
offset--
|
||||
}
|
||||
return s[:offset+1]
|
||||
}
|
||||
|
||||
// Ftoa converts a float to a string with no trailing zeros.
|
||||
func Ftoa(num float64) string {
|
||||
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64))
|
||||
}
|
||||
8
vendor/github.com/dustin/go-humanize/humanize.go
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Package humanize converts boring ugly numbers to human-friendly strings and back.
|
||||
|
||||
Durations can be turned into strings such as "3 days ago", numbers
|
||||
representing sizes like 82854982 into useful strings like, "83MB" or
|
||||
"79MiB" (whichever you prefer).
|
||||
*/
|
||||
package humanize
|
||||
192
vendor/github.com/dustin/go-humanize/number.go
generated
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
package humanize
|
||||
|
||||
/*
|
||||
Slightly adapted from the source to fit go-humanize.
|
||||
|
||||
Author: https://github.com/gorhill
|
||||
Source: https://gist.github.com/gorhill/5285193
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
renderFloatPrecisionMultipliers = [...]float64{
|
||||
1,
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
10000,
|
||||
100000,
|
||||
1000000,
|
||||
10000000,
|
||||
100000000,
|
||||
1000000000,
|
||||
}
|
||||
|
||||
renderFloatPrecisionRounders = [...]float64{
|
||||
0.5,
|
||||
0.05,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.00005,
|
||||
0.000005,
|
||||
0.0000005,
|
||||
0.00000005,
|
||||
0.000000005,
|
||||
0.0000000005,
|
||||
}
|
||||
)
|
||||
|
||||
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
|
||||
// * thousands separator
|
||||
// * decimal separator
|
||||
// * decimal precision
|
||||
//
|
||||
// Usage: s := RenderFloat(format, n)
|
||||
// The format parameter tells how to render the number n.
|
||||
//
|
||||
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
|
||||
//
|
||||
// Examples of format strings, given n = 12345.6789:
|
||||
// "#,###.##" => "12,345.67"
|
||||
// "#,###." => "12,345"
|
||||
// "#,###" => "12345,678"
|
||||
// "#\u202F###,##" => "12 345,68"
|
||||
// "#.###,###### => 12.345,678900
|
||||
// "" (aka default format) => 12,345.67
|
||||
//
|
||||
// The highest precision allowed is 9 digits after the decimal symbol.
|
||||
// There is also a version for integer number, FormatInteger(),
|
||||
// which is convenient for calls within template.
|
||||
func FormatFloat(format string, n float64) string {
|
||||
// Special cases:
|
||||
// NaN = "NaN"
|
||||
// +Inf = "+Infinity"
|
||||
// -Inf = "-Infinity"
|
||||
if math.IsNaN(n) {
|
||||
return "NaN"
|
||||
}
|
||||
if n > math.MaxFloat64 {
|
||||
return "Infinity"
|
||||
}
|
||||
if n < -math.MaxFloat64 {
|
||||
return "-Infinity"
|
||||
}
|
||||
|
||||
// default format
|
||||
precision := 2
|
||||
decimalStr := "."
|
||||
thousandStr := ","
|
||||
positiveStr := ""
|
||||
negativeStr := "-"
|
||||
|
||||
if len(format) > 0 {
|
||||
format := []rune(format)
|
||||
|
||||
// If there is an explicit format directive,
|
||||
// then default values are these:
|
||||
precision = 9
|
||||
thousandStr = ""
|
||||
|
||||
// collect indices of meaningful formatting directives
|
||||
formatIndx := []int{}
|
||||
for i, char := range format {
|
||||
if char != '#' && char != '0' {
|
||||
formatIndx = append(formatIndx, i)
|
||||
}
|
||||
}
|
||||
|
||||
if len(formatIndx) > 0 {
|
||||
// Directive at index 0:
|
||||
// Must be a '+'
|
||||
// Raise an error if not the case
|
||||
// index: 0123456789
|
||||
// +0.000,000
|
||||
// +000,000.0
|
||||
// +0000.00
|
||||
// +0000
|
||||
if formatIndx[0] == 0 {
|
||||
if format[formatIndx[0]] != '+' {
|
||||
panic("RenderFloat(): invalid positive sign directive")
|
||||
}
|
||||
positiveStr = "+"
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// Two directives:
|
||||
// First is thousands separator
|
||||
// Raise an error if not followed by 3-digit
|
||||
// 0123456789
|
||||
// 0.000,000
|
||||
// 000,000.00
|
||||
if len(formatIndx) == 2 {
|
||||
if (formatIndx[1] - formatIndx[0]) != 4 {
|
||||
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
|
||||
}
|
||||
thousandStr = string(format[formatIndx[0]])
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// One directive:
|
||||
// Directive is decimal separator
|
||||
// The number of digit-specifier following the separator indicates wanted precision
|
||||
// 0123456789
|
||||
// 0.00
|
||||
// 000,0000
|
||||
if len(formatIndx) == 1 {
|
||||
decimalStr = string(format[formatIndx[0]])
|
||||
precision = len(format) - formatIndx[0] - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate sign part
|
||||
var signStr string
|
||||
if n >= 0.000000001 {
|
||||
signStr = positiveStr
|
||||
} else if n <= -0.000000001 {
|
||||
signStr = negativeStr
|
||||
n = -n
|
||||
} else {
|
||||
signStr = ""
|
||||
n = 0.0
|
||||
}
|
||||
|
||||
// split number into integer and fractional parts
|
||||
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
|
||||
|
||||
// generate integer part string
|
||||
intStr := strconv.FormatInt(int64(intf), 10)
|
||||
|
||||
// add thousand separator if required
|
||||
if len(thousandStr) > 0 {
|
||||
for i := len(intStr); i > 3; {
|
||||
i -= 3
|
||||
intStr = intStr[:i] + thousandStr + intStr[i:]
|
||||
}
|
||||
}
|
||||
|
||||
// no fractional part, we can leave now
|
||||
if precision == 0 {
|
||||
return signStr + intStr
|
||||
}
|
||||
|
||||
// generate fractional part
|
||||
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
|
||||
// may need padding
|
||||
if len(fracStr) < precision {
|
||||
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
|
||||
}
|
||||
|
||||
return signStr + intStr + decimalStr + fracStr
|
||||
}
|
||||
|
||||
// FormatInteger produces a formatted number as string.
|
||||
// See FormatFloat.
|
||||
func FormatInteger(format string, n int) string {
|
||||
return FormatFloat(format, float64(n))
|
||||
}
|
||||
25
vendor/github.com/dustin/go-humanize/ordinals.go
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
package humanize
|
||||
|
||||
import "strconv"
|
||||
|
||||
// Ordinal gives you the input number in a rank/ordinal format.
|
||||
//
|
||||
// Ordinal(3) -> 3rd
|
||||
func Ordinal(x int) string {
|
||||
suffix := "th"
|
||||
switch x % 10 {
|
||||
case 1:
|
||||
if x%100 != 11 {
|
||||
suffix = "st"
|
||||
}
|
||||
case 2:
|
||||
if x%100 != 12 {
|
||||
suffix = "nd"
|
||||
}
|
||||
case 3:
|
||||
if x%100 != 13 {
|
||||
suffix = "rd"
|
||||
}
|
||||
}
|
||||
return strconv.Itoa(x) + suffix
|
||||
}
|
||||
113
vendor/github.com/dustin/go-humanize/si.go
generated
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var siPrefixTable = map[float64]string{
|
||||
-24: "y", // yocto
|
||||
-21: "z", // zepto
|
||||
-18: "a", // atto
|
||||
-15: "f", // femto
|
||||
-12: "p", // pico
|
||||
-9: "n", // nano
|
||||
-6: "µ", // micro
|
||||
-3: "m", // milli
|
||||
0: "",
|
||||
3: "k", // kilo
|
||||
6: "M", // mega
|
||||
9: "G", // giga
|
||||
12: "T", // tera
|
||||
15: "P", // peta
|
||||
18: "E", // exa
|
||||
21: "Z", // zetta
|
||||
24: "Y", // yotta
|
||||
}
|
||||
|
||||
var revSIPrefixTable = revfmap(siPrefixTable)
|
||||
|
||||
// revfmap reverses the map and precomputes the power multiplier
|
||||
func revfmap(in map[float64]string) map[string]float64 {
|
||||
rv := map[string]float64{}
|
||||
for k, v := range in {
|
||||
rv[v] = math.Pow(10, k)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
var riParseRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
ri := `^([\-0-9.]+)\s?([`
|
||||
for _, v := range siPrefixTable {
|
||||
ri += v
|
||||
}
|
||||
ri += `]?)(.*)`
|
||||
|
||||
riParseRegex = regexp.MustCompile(ri)
|
||||
}
|
||||
|
||||
// ComputeSI finds the most appropriate SI prefix for the given number
|
||||
// and returns the prefix along with the value adjusted to be within
|
||||
// that prefix.
|
||||
//
|
||||
// See also: SI, ParseSI.
|
||||
//
|
||||
// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p")
|
||||
func ComputeSI(input float64) (float64, string) {
|
||||
if input == 0 {
|
||||
return 0, ""
|
||||
}
|
||||
mag := math.Abs(input)
|
||||
exponent := math.Floor(logn(mag, 10))
|
||||
exponent = math.Floor(exponent/3) * 3
|
||||
|
||||
value := mag / math.Pow(10, exponent)
|
||||
|
||||
// Handle special case where value is exactly 1000.0
|
||||
// Should return 1M instead of 1000k
|
||||
if value == 1000.0 {
|
||||
exponent += 3
|
||||
value = mag / math.Pow(10, exponent)
|
||||
}
|
||||
|
||||
value = math.Copysign(value, input)
|
||||
|
||||
prefix := siPrefixTable[exponent]
|
||||
return value, prefix
|
||||
}
|
||||
|
||||
// SI returns a string with default formatting.
|
||||
//
|
||||
// SI uses Ftoa to format float value, removing trailing zeros.
|
||||
//
|
||||
// See also: ComputeSI, ParseSI.
|
||||
//
|
||||
// e.g. SI(1000000, B) -> 1MB
|
||||
// e.g. SI(2.2345e-12, "F") -> 2.2345pF
|
||||
func SI(input float64, unit string) string {
|
||||
value, prefix := ComputeSI(input)
|
||||
return Ftoa(value) + " " + prefix + unit
|
||||
}
|
||||
|
||||
var errInvalid = errors.New("invalid input")
|
||||
|
||||
// ParseSI parses an SI string back into the number and unit.
|
||||
//
|
||||
// See also: SI, ComputeSI.
|
||||
//
|
||||
// e.g. ParseSI(2.2345pF) -> (2.2345e-12, "F", nil)
|
||||
func ParseSI(input string) (float64, string, error) {
|
||||
found := riParseRegex.FindStringSubmatch(input)
|
||||
if len(found) != 4 {
|
||||
return 0, "", errInvalid
|
||||
}
|
||||
mag := revSIPrefixTable[found[2]]
|
||||
unit := found[3]
|
||||
|
||||
base, err := strconv.ParseFloat(found[1], 64)
|
||||
return base * mag, unit, err
|
||||
}
|
||||
117
vendor/github.com/dustin/go-humanize/times.go
generated
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Seconds-based time units
|
||||
const (
|
||||
Day = 24 * time.Hour
|
||||
Week = 7 * Day
|
||||
Month = 30 * Day
|
||||
Year = 12 * Month
|
||||
LongTime = 37 * Year
|
||||
)
|
||||
|
||||
// Time formats a time into a relative string.
|
||||
//
|
||||
// Time(someT) -> "3 weeks ago"
|
||||
func Time(then time.Time) string {
|
||||
return RelTime(then, time.Now(), "ago", "from now")
|
||||
}
|
||||
|
||||
// A RelTimeMagnitude struct contains a relative time point at which
|
||||
// the relative format of time will switch to a new format string. A
|
||||
// slice of these in ascending order by their "D" field is passed to
|
||||
// CustomRelTime to format durations.
|
||||
//
|
||||
// The Format field is a string that may contain a "%s" which will be
|
||||
// replaced with the appropriate signed label (e.g. "ago" or "from
|
||||
// now") and a "%d" that will be replaced by the quantity.
|
||||
//
|
||||
// The DivBy field is the amount of time the time difference must be
|
||||
// divided by in order to display correctly.
|
||||
//
|
||||
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
|
||||
// DivBy should be time.Minute so whatever the duration is will be
|
||||
// expressed in minutes.
|
||||
type RelTimeMagnitude struct {
|
||||
D time.Duration
|
||||
Format string
|
||||
DivBy time.Duration
|
||||
}
|
||||
|
||||
var defaultMagnitudes = []RelTimeMagnitude{
|
||||
{time.Second, "now", time.Second},
|
||||
{2 * time.Second, "1 second %s", 1},
|
||||
{time.Minute, "%d seconds %s", time.Second},
|
||||
{2 * time.Minute, "1 minute %s", 1},
|
||||
{time.Hour, "%d minutes %s", time.Minute},
|
||||
{2 * time.Hour, "1 hour %s", 1},
|
||||
{Day, "%d hours %s", time.Hour},
|
||||
{2 * Day, "1 day %s", 1},
|
||||
{Week, "%d days %s", Day},
|
||||
{2 * Week, "1 week %s", 1},
|
||||
{Month, "%d weeks %s", Week},
|
||||
{2 * Month, "1 month %s", 1},
|
||||
{Year, "%d months %s", Month},
|
||||
{18 * Month, "1 year %s", 1},
|
||||
{2 * Year, "2 years %s", 1},
|
||||
{LongTime, "%d years %s", Year},
|
||||
{math.MaxInt64, "a long while %s", 1},
|
||||
}
|
||||
|
||||
// RelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times and two labels. In addition to the generic time
|
||||
// delta string (e.g. 5 minutes), the labels are used applied so that
|
||||
// the label corresponding to the smaller time is applied.
|
||||
//
|
||||
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
|
||||
func RelTime(a, b time.Time, albl, blbl string) string {
|
||||
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
|
||||
}
|
||||
|
||||
// CustomRelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times two labels and a table of relative time formats.
|
||||
// In addition to the generic time delta string (e.g. 5 minutes), the
|
||||
// labels are used applied so that the label corresponding to the
|
||||
// smaller time is applied.
|
||||
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
|
||||
lbl := albl
|
||||
diff := b.Sub(a)
|
||||
|
||||
if a.After(b) {
|
||||
lbl = blbl
|
||||
diff = a.Sub(b)
|
||||
}
|
||||
|
||||
n := sort.Search(len(magnitudes), func(i int) bool {
|
||||
return magnitudes[i].D >= diff
|
||||
})
|
||||
|
||||
if n >= len(magnitudes) {
|
||||
n = len(magnitudes) - 1
|
||||
}
|
||||
mag := magnitudes[n]
|
||||
args := []interface{}{}
|
||||
escaped := false
|
||||
for _, ch := range mag.Format {
|
||||
if escaped {
|
||||
switch ch {
|
||||
case 's':
|
||||
args = append(args, lbl)
|
||||
case 'd':
|
||||
args = append(args, diff/mag.DivBy)
|
||||
}
|
||||
escaped = false
|
||||
} else {
|
||||
escaped = ch == '%'
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(mag.Format, args...)
|
||||
}
|
||||
2
vendor/github.com/gliderlabs/ssh/README.md
generated
vendored
@@ -14,7 +14,7 @@ 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:
|
||||
|
||||
```
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
6
vendor/github.com/gliderlabs/ssh/agent.go
generated
vendored
@@ -22,8 +22,10 @@ const (
|
||||
// client requested agent forwarding
|
||||
var contextKeyAgentRequest = &contextKey{"auth-agent-req"}
|
||||
|
||||
func setAgentRequested(sess *session) {
|
||||
sess.ctx.SetValue(contextKeyAgentRequest, true)
|
||||
// SetAgentRequested sets up the session context so that AgentRequested
|
||||
// returns true.
|
||||
func SetAgentRequested(ctx Context) {
|
||||
ctx.SetValue(contextKeyAgentRequest, true)
|
||||
}
|
||||
|
||||
// AgentRequested returns true if the client requested agent forwarding.
|
||||
|
||||
2
vendor/github.com/gliderlabs/ssh/context.go
generated
vendored
@@ -103,7 +103,7 @@ func newContext(srv *Server) (*sshContext, context.CancelFunc) {
|
||||
|
||||
// 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) {
|
||||
func applyConnMetadata(ctx Context, conn gossh.ConnMetadata) {
|
||||
if ctx.Value(ContextKeySessionID) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
38
vendor/github.com/gliderlabs/ssh/server.go
generated
vendored
@@ -26,6 +26,7 @@ type Server struct {
|
||||
|
||||
PasswordHandler PasswordHandler // password authentication handler
|
||||
PublicKeyHandler PublicKeyHandler // public key authentication handler
|
||||
ChannelHandler ChannelHandler // channel 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
|
||||
@@ -33,16 +34,13 @@ type Server struct {
|
||||
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)
|
||||
type ChannelHandler func(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context)
|
||||
|
||||
func (srv *Server) ensureHostSigner() error {
|
||||
if len(srv.HostSigners) == 0 {
|
||||
@@ -55,11 +53,7 @@ func (srv *Server) ensureHostSigner() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *Server) config(ctx *sshContext) *gossh.ServerConfig {
|
||||
srv.channelHandlers = map[string]channelHandler{
|
||||
"session": sessionHandler,
|
||||
"direct-tcpip": directTcpipHandler,
|
||||
}
|
||||
func (srv *Server) config(ctx Context) *gossh.ServerConfig {
|
||||
config := &gossh.ServerConfig{}
|
||||
for _, signer := range srv.HostSigners {
|
||||
config.AddHostKey(signer)
|
||||
@@ -72,7 +66,7 @@ func (srv *Server) config(ctx *sshContext) *gossh.ServerConfig {
|
||||
}
|
||||
if srv.PasswordHandler != nil {
|
||||
config.PasswordCallback = func(conn gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) {
|
||||
ctx.applyConnMetadata(conn)
|
||||
applyConnMetadata(ctx, conn)
|
||||
if ok := srv.PasswordHandler(ctx, string(password)); !ok {
|
||||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
|
||||
}
|
||||
@@ -81,7 +75,7 @@ func (srv *Server) config(ctx *sshContext) *gossh.ServerConfig {
|
||||
}
|
||||
if srv.PublicKeyHandler != nil {
|
||||
config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
|
||||
ctx.applyConnMetadata(conn)
|
||||
applyConnMetadata(ctx, conn)
|
||||
if ok := srv.PublicKeyHandler(ctx, key); !ok {
|
||||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
|
||||
}
|
||||
@@ -223,15 +217,25 @@ func (srv *Server) handleConn(newConn net.Conn) {
|
||||
defer srv.trackConn(sshConn, false)
|
||||
|
||||
ctx.SetValue(ContextKeyConn, sshConn)
|
||||
ctx.applyConnMetadata(sshConn)
|
||||
applyConnMetadata(ctx, 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
|
||||
if srv.ChannelHandler == nil {
|
||||
DefaultChannelHandler(srv, sshConn, ch, ctx)
|
||||
} else {
|
||||
srv.ChannelHandler(srv, sshConn, ch, ctx)
|
||||
}
|
||||
go handler(srv, sshConn, ch, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultChannelHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
|
||||
switch newChan.ChannelType() {
|
||||
case "session":
|
||||
go sessionHandler(srv, conn, newChan, ctx)
|
||||
case "direct-tcpip":
|
||||
go directTcpipHandler(srv, conn, newChan, ctx)
|
||||
default:
|
||||
newChan.Reject(gossh.UnknownChannelType, "unsupported channel type")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
vendor/github.com/gliderlabs/ssh/session.go
generated
vendored
@@ -71,27 +71,24 @@ type Session interface {
|
||||
// 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) {
|
||||
func sessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
|
||||
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,
|
||||
Channel: ch,
|
||||
conn: conn,
|
||||
handler: srv.Handler,
|
||||
ptyCb: srv.PtyCallback,
|
||||
ctx: ctx,
|
||||
}
|
||||
sess.handleRequests(reqs)
|
||||
}
|
||||
@@ -99,19 +96,18 @@ func sessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChanne
|
||||
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
|
||||
conn *gossh.ServerConn
|
||||
handler Handler
|
||||
handled bool
|
||||
exited bool
|
||||
pty *Pty
|
||||
winch chan Window
|
||||
env []string
|
||||
ptyCb PtyCallback
|
||||
cmd []string
|
||||
ctx Context
|
||||
sigCh chan<- Signal
|
||||
sigBuf []Signal
|
||||
}
|
||||
|
||||
func (sess *session) Write(p []byte) (n int, err error) {
|
||||
@@ -146,13 +142,12 @@ func (sess *session) Permissions() Permissions {
|
||||
}
|
||||
|
||||
func (sess *session) Context() context.Context {
|
||||
return sess.ctx.Context
|
||||
return sess.ctx
|
||||
}
|
||||
|
||||
func (sess *session) Exit(code int) error {
|
||||
sess.Lock()
|
||||
defer sess.Unlock()
|
||||
|
||||
if sess.exited {
|
||||
return errors.New("Session.Exit called multiple times")
|
||||
}
|
||||
@@ -163,9 +158,6 @@ func (sess *session) Exit(code int) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
close(sess.maskedReqs)
|
||||
|
||||
return sess.Close()
|
||||
}
|
||||
|
||||
@@ -209,10 +201,6 @@ func (sess *session) Signals(c chan<- Signal) {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -290,12 +278,10 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
|
||||
req.Reply(ok, nil)
|
||||
case agentRequestType:
|
||||
// TODO: option/callback to allow agent forwarding
|
||||
setAgentRequested(sess)
|
||||
SetAgentRequested(sess.ctx)
|
||||
req.Reply(true, nil)
|
||||
default:
|
||||
// TODO: debug log
|
||||
}
|
||||
|
||||
sess.maskedReqs <- req
|
||||
}
|
||||
}
|
||||
|
||||
2
vendor/github.com/gliderlabs/ssh/tcpip.go
generated
vendored
@@ -17,7 +17,7 @@ type forwardData struct {
|
||||
OriginatorPort uint32
|
||||
}
|
||||
|
||||
func directTcpipHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx *sshContext) {
|
||||
func directTcpipHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
|
||||
d := forwardData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error())
|
||||
|
||||
2
vendor/github.com/gliderlabs/ssh/util.go
generated
vendored
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func generateSigner() (ssh.Signer, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 768)
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
23
vendor/github.com/kr/pty/License
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
Copyright (c) 2011 Keith Rarick
|
||||
|
||||
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.
|
||||
36
vendor/github.com/kr/pty/README.md
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# pty
|
||||
|
||||
Pty is a Go package for using unix pseudo-terminals.
|
||||
|
||||
## Install
|
||||
|
||||
go get github.com/kr/pty
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/kr/pty"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := exec.Command("grep", "--color=auto", "bar")
|
||||
f, err := pty.Start(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
f.Write([]byte("foo\n"))
|
||||
f.Write([]byte("bar\n"))
|
||||
f.Write([]byte("baz\n"))
|
||||
f.Write([]byte{4}) // EOT
|
||||
}()
|
||||
io.Copy(os.Stdout, f)
|
||||
}
|
||||
```
|
||||
16
vendor/github.com/kr/pty/doc.go
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
// Package pty provides functions for working with Unix terminals.
|
||||
package pty
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ErrUnsupported is returned if a function is not
|
||||
// available on the current platform.
|
||||
var ErrUnsupported = errors.New("unsupported")
|
||||
|
||||
// Opens a pty and its corresponding tty.
|
||||
func Open() (pty, tty *os.File, err error) {
|
||||
return open()
|
||||
}
|
||||
13
vendor/github.com/kr/pty/ioctl.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// +build !windows
|
||||
|
||||
package pty
|
||||
|
||||
import "syscall"
|
||||
|
||||
func ioctl(fd, cmd, ptr uintptr) error {
|
||||
_, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr)
|
||||
if e != 0 {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
39
vendor/github.com/kr/pty/ioctl_bsd.go
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
// +build darwin dragonfly freebsd netbsd openbsd
|
||||
|
||||
package pty
|
||||
|
||||
// from <sys/ioccom.h>
|
||||
const (
|
||||
_IOC_VOID uintptr = 0x20000000
|
||||
_IOC_OUT uintptr = 0x40000000
|
||||
_IOC_IN uintptr = 0x80000000
|
||||
_IOC_IN_OUT uintptr = _IOC_OUT | _IOC_IN
|
||||
_IOC_DIRMASK = _IOC_VOID | _IOC_OUT | _IOC_IN
|
||||
|
||||
_IOC_PARAM_SHIFT = 13
|
||||
_IOC_PARAM_MASK = (1 << _IOC_PARAM_SHIFT) - 1
|
||||
)
|
||||
|
||||
func _IOC_PARM_LEN(ioctl uintptr) uintptr {
|
||||
return (ioctl >> 16) & _IOC_PARAM_MASK
|
||||
}
|
||||
|
||||
func _IOC(inout uintptr, group byte, ioctl_num uintptr, param_len uintptr) uintptr {
|
||||
return inout | (param_len&_IOC_PARAM_MASK)<<16 | uintptr(group)<<8 | ioctl_num
|
||||
}
|
||||
|
||||
func _IO(group byte, ioctl_num uintptr) uintptr {
|
||||
return _IOC(_IOC_VOID, group, ioctl_num, 0)
|
||||
}
|
||||
|
||||
func _IOR(group byte, ioctl_num uintptr, param_len uintptr) uintptr {
|
||||
return _IOC(_IOC_OUT, group, ioctl_num, param_len)
|
||||
}
|
||||
|
||||
func _IOW(group byte, ioctl_num uintptr, param_len uintptr) uintptr {
|
||||
return _IOC(_IOC_IN, group, ioctl_num, param_len)
|
||||
}
|
||||
|
||||
func _IOWR(group byte, ioctl_num uintptr, param_len uintptr) uintptr {
|
||||
return _IOC(_IOC_IN_OUT, group, ioctl_num, param_len)
|
||||
}
|
||||
19
vendor/github.com/kr/pty/mktypes.bash
generated
vendored
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
GOOSARCH="${GOOS}_${GOARCH}"
|
||||
case "$GOOSARCH" in
|
||||
_* | *_ | _)
|
||||
echo 'undefined $GOOS_$GOARCH:' "$GOOSARCH" 1>&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
GODEFS="go tool cgo -godefs"
|
||||
|
||||
$GODEFS types.go |gofmt > ztypes_$GOARCH.go
|
||||
|
||||
case $GOOS in
|
||||
freebsd|dragonfly)
|
||||
$GODEFS types_$GOOS.go |gofmt > ztypes_$GOOSARCH.go
|
||||
;;
|
||||
esac
|
||||
60
vendor/github.com/kr/pty/pty_darwin.go
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
package pty
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func open() (pty, tty *os.File, err error) {
|
||||
p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sname, err := ptsname(p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = grantpt(p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = unlockpt(p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
t, err := os.OpenFile(sname, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return p, t, nil
|
||||
}
|
||||
|
||||
func ptsname(f *os.File) (string, error) {
|
||||
n := make([]byte, _IOC_PARM_LEN(syscall.TIOCPTYGNAME))
|
||||
|
||||
err := ioctl(f.Fd(), syscall.TIOCPTYGNAME, uintptr(unsafe.Pointer(&n[0])))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i, c := range n {
|
||||
if c == 0 {
|
||||
return string(n[:i]), nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("TIOCPTYGNAME string not NUL-terminated")
|
||||
}
|
||||
|
||||
func grantpt(f *os.File) error {
|
||||
return ioctl(f.Fd(), syscall.TIOCPTYGRANT, 0)
|
||||
}
|
||||
|
||||
func unlockpt(f *os.File) error {
|
||||
return ioctl(f.Fd(), syscall.TIOCPTYUNLK, 0)
|
||||
}
|
||||
76
vendor/github.com/kr/pty/pty_dragonfly.go
generated
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
package pty
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// same code as pty_darwin.go
|
||||
func open() (pty, tty *os.File, err error) {
|
||||
p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sname, err := ptsname(p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = grantpt(p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = unlockpt(p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
t, err := os.OpenFile(sname, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return p, t, nil
|
||||
}
|
||||
|
||||
func grantpt(f *os.File) error {
|
||||
_, err := isptmaster(f.Fd())
|
||||
return err
|
||||
}
|
||||
|
||||
func unlockpt(f *os.File) error {
|
||||
_, err := isptmaster(f.Fd())
|
||||
return err
|
||||
}
|
||||
|
||||
func isptmaster(fd uintptr) (bool, error) {
|
||||
err := ioctl(fd, syscall.TIOCISPTMASTER, 0)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
emptyFiodgnameArg fiodgnameArg
|
||||
ioctl_FIODNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg))
|
||||
)
|
||||
|
||||
func ptsname(f *os.File) (string, error) {
|
||||
name := make([]byte, _C_SPECNAMELEN)
|
||||
fa := fiodgnameArg{Name: (*byte)(unsafe.Pointer(&name[0])), Len: _C_SPECNAMELEN, Pad_cgo_0: [4]byte{0, 0, 0, 0}}
|
||||
|
||||
err := ioctl(f.Fd(), ioctl_FIODNAME, uintptr(unsafe.Pointer(&fa)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i, c := range name {
|
||||
if c == 0 {
|
||||
s := "/dev/" + string(name[:i])
|
||||
return strings.Replace(s, "ptm", "pts", -1), nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("TIOCPTYGNAME string not NUL-terminated")
|
||||
}
|
||||
73
vendor/github.com/kr/pty/pty_freebsd.go
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
package pty
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func posix_openpt(oflag int) (fd int, err error) {
|
||||
r0, _, e1 := syscall.Syscall(syscall.SYS_POSIX_OPENPT, uintptr(oflag), 0, 0)
|
||||
fd = int(r0)
|
||||
if e1 != 0 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func open() (pty, tty *os.File, err error) {
|
||||
fd, err := posix_openpt(syscall.O_RDWR | syscall.O_CLOEXEC)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
p := os.NewFile(uintptr(fd), "/dev/pts")
|
||||
sname, err := ptsname(p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
t, err := os.OpenFile("/dev/"+sname, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return p, t, nil
|
||||
}
|
||||
|
||||
func isptmaster(fd uintptr) (bool, error) {
|
||||
err := ioctl(fd, syscall.TIOCPTMASTER, 0)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
emptyFiodgnameArg fiodgnameArg
|
||||
ioctl_FIODGNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg))
|
||||
)
|
||||
|
||||
func ptsname(f *os.File) (string, error) {
|
||||
master, err := isptmaster(f.Fd())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !master {
|
||||
return "", syscall.EINVAL
|
||||
}
|
||||
|
||||
const n = _C_SPECNAMELEN + 1
|
||||
var (
|
||||
buf = make([]byte, n)
|
||||
arg = fiodgnameArg{Len: n, Buf: (*byte)(unsafe.Pointer(&buf[0]))}
|
||||
)
|
||||
err = ioctl(f.Fd(), ioctl_FIODGNAME, uintptr(unsafe.Pointer(&arg)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i, c := range buf {
|
||||
if c == 0 {
|
||||
return string(buf[:i]), nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("FIODGNAME string not NUL-terminated")
|
||||
}
|
||||
46
vendor/github.com/kr/pty/pty_linux.go
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
package pty
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func open() (pty, tty *os.File, err error) {
|
||||
p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sname, err := ptsname(p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = unlockpt(p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
t, err := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return p, t, nil
|
||||
}
|
||||
|
||||
func ptsname(f *os.File) (string, error) {
|
||||
var n _C_uint
|
||||
err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "/dev/pts/" + strconv.Itoa(int(n)), nil
|
||||
}
|
||||
|
||||
func unlockpt(f *os.File) error {
|
||||
var u _C_int
|
||||
// use TIOCSPTLCK with a zero valued arg to clear the slave pty lock
|
||||
return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
|
||||
}
|
||||
11
vendor/github.com/kr/pty/pty_unsupported.go
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// +build !linux,!darwin,!freebsd,!dragonfly
|
||||
|
||||
package pty
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func open() (pty, tty *os.File, err error) {
|
||||
return nil, nil, ErrUnsupported
|
||||
}
|
||||
34
vendor/github.com/kr/pty/run.go
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
// +build !windows
|
||||
|
||||
package pty
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Start assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
|
||||
// and c.Stderr, calls c.Start, and returns the File of the tty's
|
||||
// corresponding pty.
|
||||
func Start(c *exec.Cmd) (pty *os.File, err error) {
|
||||
pty, tty, err := Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tty.Close()
|
||||
c.Stdout = tty
|
||||
c.Stdin = tty
|
||||
c.Stderr = tty
|
||||
if c.SysProcAttr == nil {
|
||||
c.SysProcAttr = &syscall.SysProcAttr{}
|
||||
}
|
||||
c.SysProcAttr.Setctty = true
|
||||
c.SysProcAttr.Setsid = true
|
||||
err = c.Start()
|
||||
if err != nil {
|
||||
pty.Close()
|
||||
return nil, err
|
||||
}
|
||||
return pty, err
|
||||
}
|
||||
10
vendor/github.com/kr/pty/types.go
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
// +build ignore
|
||||
|
||||
package pty
|
||||
|
||||
import "C"
|
||||
|
||||
type (
|
||||
_C_int C.int
|
||||
_C_uint C.uint
|
||||
)
|
||||
17
vendor/github.com/kr/pty/types_dragonfly.go
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
// +build ignore
|
||||
|
||||
package pty
|
||||
|
||||
/*
|
||||
#define _KERNEL
|
||||
#include <sys/conf.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/filio.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
const (
|
||||
_C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */
|
||||
)
|
||||
|
||||
type fiodgnameArg C.struct_fiodname_args
|
||||
15
vendor/github.com/kr/pty/types_freebsd.go
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
// +build ignore
|
||||
|
||||
package pty
|
||||
|
||||
/*
|
||||
#include <sys/param.h>
|
||||
#include <sys/filio.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
const (
|
||||
_C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */
|
||||
)
|
||||
|
||||
type fiodgnameArg C.struct_fiodgname_arg
|
||||
37
vendor/github.com/kr/pty/util.go
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
// +build !windows
|
||||
|
||||
package pty
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Getsize returns the number of rows (lines) and cols (positions
|
||||
// in each line) in terminal t.
|
||||
func Getsize(t *os.File) (rows, cols int, err error) {
|
||||
var ws winsize
|
||||
err = windowrect(&ws, t.Fd())
|
||||
return int(ws.ws_row), int(ws.ws_col), err
|
||||
}
|
||||
|
||||
type winsize struct {
|
||||
ws_row uint16
|
||||
ws_col uint16
|
||||
ws_xpixel uint16
|
||||
ws_ypixel uint16
|
||||
}
|
||||
|
||||
func windowrect(ws *winsize, fd uintptr) error {
|
||||
_, _, errno := syscall.Syscall(
|
||||
syscall.SYS_IOCTL,
|
||||
fd,
|
||||
syscall.TIOCGWINSZ,
|
||||
uintptr(unsafe.Pointer(ws)),
|
||||
)
|
||||
if errno != 0 {
|
||||
return syscall.Errno(errno)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
9
vendor/github.com/kr/pty/ztypes_386.go
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs types.go
|
||||
|
||||
package pty
|
||||
|
||||
type (
|
||||
_C_int int32
|
||||
_C_uint uint32
|
||||
)
|
||||
9
vendor/github.com/kr/pty/ztypes_amd64.go
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs types.go
|
||||
|
||||
package pty
|
||||
|
||||
type (
|
||||
_C_int int32
|
||||
_C_uint uint32
|
||||
)
|
||||
9
vendor/github.com/kr/pty/ztypes_arm.go
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs types.go
|
||||
|
||||
package pty
|
||||
|
||||
type (
|
||||
_C_int int32
|
||||
_C_uint uint32
|
||||
)
|
||||
11
vendor/github.com/kr/pty/ztypes_arm64.go
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs types.go
|
||||
|
||||
// +build arm64
|
||||
|
||||
package pty
|
||||
|
||||
type (
|
||||
_C_int int32
|
||||
_C_uint uint32
|
||||
)
|
||||
14
vendor/github.com/kr/pty/ztypes_dragonfly_amd64.go
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs types_dragonfly.go
|
||||
|
||||
package pty
|
||||
|
||||
const (
|
||||
_C_SPECNAMELEN = 0x3f
|
||||
)
|
||||
|
||||
type fiodgnameArg struct {
|
||||
Name *byte
|
||||
Len uint32
|
||||
Pad_cgo_0 [4]byte
|
||||
}
|
||||
13
vendor/github.com/kr/pty/ztypes_freebsd_386.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs types_freebsd.go
|
||||
|
||||
package pty
|
||||
|
||||
const (
|
||||
_C_SPECNAMELEN = 0x3f
|
||||
)
|
||||
|
||||
type fiodgnameArg struct {
|
||||
Len int32
|
||||
Buf *byte
|
||||
}
|
||||
14
vendor/github.com/kr/pty/ztypes_freebsd_amd64.go
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs types_freebsd.go
|
||||
|
||||
package pty
|
||||
|
||||
const (
|
||||
_C_SPECNAMELEN = 0x3f
|
||||
)
|
||||
|
||||
type fiodgnameArg struct {
|
||||
Len int32
|
||||
Pad_cgo_0 [4]byte
|
||||
Buf *byte
|
||||
}
|
||||
13
vendor/github.com/kr/pty/ztypes_freebsd_arm.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs types_freebsd.go
|
||||
|
||||
package pty
|
||||
|
||||
const (
|
||||
_C_SPECNAMELEN = 0x3f
|
||||
)
|
||||
|
||||
type fiodgnameArg struct {
|
||||
Len int32
|
||||
Buf *byte
|
||||
}
|
||||
12
vendor/github.com/kr/pty/ztypes_mipsx.go
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs types.go
|
||||
|
||||
// +build linux
|
||||
// +build mips mipsle mips64 mips64le
|
||||
|
||||
package pty
|
||||
|
||||
type (
|
||||
_C_int int32
|
||||
_C_uint uint32
|
||||
)
|
||||
11
vendor/github.com/kr/pty/ztypes_ppc64.go
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// +build ppc64
|
||||
|
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs types.go
|
||||
|
||||
package pty
|
||||
|
||||
type (
|
||||
_C_int int32
|
||||
_C_uint uint32
|
||||
)
|
||||
11
vendor/github.com/kr/pty/ztypes_ppc64le.go
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// +build ppc64le
|
||||
|
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs types.go
|
||||
|
||||
package pty
|
||||
|
||||
type (
|
||||
_C_int int32
|
||||
_C_uint uint32
|
||||
)
|
||||
11
vendor/github.com/kr/pty/ztypes_s390x.go
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// +build s390x
|
||||
|
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs types.go
|
||||
|
||||
package pty
|
||||
|
||||
type (
|
||||
_C_int int32
|
||||
_C_uint uint32
|
||||
)
|
||||
21
vendor/github.com/mattn/go-colorable/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Yasuhiro Matsumoto
|
||||
|
||||
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.
|
||||
48
vendor/github.com/mattn/go-colorable/README.md
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
# go-colorable
|
||||
|
||||
[](http://godoc.org/github.com/mattn/go-colorable)
|
||||
[](https://travis-ci.org/mattn/go-colorable)
|
||||
[](https://coveralls.io/github/mattn/go-colorable?branch=master)
|
||||
[](https://goreportcard.com/report/mattn/go-colorable)
|
||||
|
||||
Colorable writer for windows.
|
||||
|
||||
For example, most of logger packages doesn't show colors on windows. (I know we can do it with ansicon. But I don't want.)
|
||||
This package is possible to handle escape sequence for ansi color on windows.
|
||||
|
||||
## Too Bad!
|
||||
|
||||

|
||||
|
||||
|
||||
## So Good!
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})
|
||||
logrus.SetOutput(colorable.NewColorableStdout())
|
||||
|
||||
logrus.Info("succeeded")
|
||||
logrus.Warn("not correct")
|
||||
logrus.Error("something error")
|
||||
logrus.Fatal("panic")
|
||||
```
|
||||
|
||||
You can compile above code on non-windows OSs.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
$ go get github.com/mattn/go-colorable
|
||||
```
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
|
||||
# Author
|
||||
|
||||
Yasuhiro Matsumoto (a.k.a mattn)
|
||||
29
vendor/github.com/mattn/go-colorable/colorable_appengine.go
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
// +build appengine
|
||||
|
||||
package colorable
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
_ "github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
// NewColorable return new instance of Writer which handle escape sequence.
|
||||
func NewColorable(file *os.File) io.Writer {
|
||||
if file == nil {
|
||||
panic("nil passed instead of *os.File to NewColorable()")
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
// NewColorableStdout return new instance of Writer which handle escape sequence for stdout.
|
||||
func NewColorableStdout() io.Writer {
|
||||
return os.Stdout
|
||||
}
|
||||
|
||||
// NewColorableStderr return new instance of Writer which handle escape sequence for stderr.
|
||||
func NewColorableStderr() io.Writer {
|
||||
return os.Stderr
|
||||
}
|
||||
30
vendor/github.com/mattn/go-colorable/colorable_others.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
// +build !windows
|
||||
// +build !appengine
|
||||
|
||||
package colorable
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
_ "github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
// NewColorable return new instance of Writer which handle escape sequence.
|
||||
func NewColorable(file *os.File) io.Writer {
|
||||
if file == nil {
|
||||
panic("nil passed instead of *os.File to NewColorable()")
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
// NewColorableStdout return new instance of Writer which handle escape sequence for stdout.
|
||||
func NewColorableStdout() io.Writer {
|
||||
return os.Stdout
|
||||
}
|
||||
|
||||
// NewColorableStderr return new instance of Writer which handle escape sequence for stderr.
|
||||
func NewColorableStderr() io.Writer {
|
||||
return os.Stderr
|
||||
}
|
||||
978
vendor/github.com/mattn/go-colorable/colorable_windows.go
generated
vendored
Normal file
@@ -0,0 +1,978 @@
|
||||
// +build windows
|
||||
// +build !appengine
|
||||
|
||||
package colorable
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
const (
|
||||
foregroundBlue = 0x1
|
||||
foregroundGreen = 0x2
|
||||
foregroundRed = 0x4
|
||||
foregroundIntensity = 0x8
|
||||
foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity)
|
||||
backgroundBlue = 0x10
|
||||
backgroundGreen = 0x20
|
||||
backgroundRed = 0x40
|
||||
backgroundIntensity = 0x80
|
||||
backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity)
|
||||
)
|
||||
|
||||
const (
|
||||
genericRead = 0x80000000
|
||||
genericWrite = 0x40000000
|
||||
)
|
||||
|
||||
const (
|
||||
consoleTextmodeBuffer = 0x1
|
||||
)
|
||||
|
||||
type wchar uint16
|
||||
type short int16
|
||||
type dword uint32
|
||||
type word uint16
|
||||
|
||||
type coord struct {
|
||||
x short
|
||||
y short
|
||||
}
|
||||
|
||||
type smallRect struct {
|
||||
left short
|
||||
top short
|
||||
right short
|
||||
bottom short
|
||||
}
|
||||
|
||||
type consoleScreenBufferInfo struct {
|
||||
size coord
|
||||
cursorPosition coord
|
||||
attributes word
|
||||
window smallRect
|
||||
maximumWindowSize coord
|
||||
}
|
||||
|
||||
type consoleCursorInfo struct {
|
||||
size dword
|
||||
visible int32
|
||||
}
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
|
||||
procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute")
|
||||
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
|
||||
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
|
||||
procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute")
|
||||
procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo")
|
||||
procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo")
|
||||
procSetConsoleTitle = kernel32.NewProc("SetConsoleTitleW")
|
||||
procCreateConsoleScreenBuffer = kernel32.NewProc("CreateConsoleScreenBuffer")
|
||||
)
|
||||
|
||||
// Writer provide colorable Writer to the console
|
||||
type Writer struct {
|
||||
out io.Writer
|
||||
handle syscall.Handle
|
||||
althandle syscall.Handle
|
||||
oldattr word
|
||||
oldpos coord
|
||||
rest bytes.Buffer
|
||||
}
|
||||
|
||||
// NewColorable return new instance of Writer which handle escape sequence from File.
|
||||
func NewColorable(file *os.File) io.Writer {
|
||||
if file == nil {
|
||||
panic("nil passed instead of *os.File to NewColorable()")
|
||||
}
|
||||
|
||||
if isatty.IsTerminal(file.Fd()) {
|
||||
var csbi consoleScreenBufferInfo
|
||||
handle := syscall.Handle(file.Fd())
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
return &Writer{out: file, handle: handle, oldattr: csbi.attributes, oldpos: coord{0, 0}}
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
// NewColorableStdout return new instance of Writer which handle escape sequence for stdout.
|
||||
func NewColorableStdout() io.Writer {
|
||||
return NewColorable(os.Stdout)
|
||||
}
|
||||
|
||||
// NewColorableStderr return new instance of Writer which handle escape sequence for stderr.
|
||||
func NewColorableStderr() io.Writer {
|
||||
return NewColorable(os.Stderr)
|
||||
}
|
||||
|
||||
var color256 = map[int]int{
|
||||
0: 0x000000,
|
||||
1: 0x800000,
|
||||
2: 0x008000,
|
||||
3: 0x808000,
|
||||
4: 0x000080,
|
||||
5: 0x800080,
|
||||
6: 0x008080,
|
||||
7: 0xc0c0c0,
|
||||
8: 0x808080,
|
||||
9: 0xff0000,
|
||||
10: 0x00ff00,
|
||||
11: 0xffff00,
|
||||
12: 0x0000ff,
|
||||
13: 0xff00ff,
|
||||
14: 0x00ffff,
|
||||
15: 0xffffff,
|
||||
16: 0x000000,
|
||||
17: 0x00005f,
|
||||
18: 0x000087,
|
||||
19: 0x0000af,
|
||||
20: 0x0000d7,
|
||||
21: 0x0000ff,
|
||||
22: 0x005f00,
|
||||
23: 0x005f5f,
|
||||
24: 0x005f87,
|
||||
25: 0x005faf,
|
||||
26: 0x005fd7,
|
||||
27: 0x005fff,
|
||||
28: 0x008700,
|
||||
29: 0x00875f,
|
||||
30: 0x008787,
|
||||
31: 0x0087af,
|
||||
32: 0x0087d7,
|
||||
33: 0x0087ff,
|
||||
34: 0x00af00,
|
||||
35: 0x00af5f,
|
||||
36: 0x00af87,
|
||||
37: 0x00afaf,
|
||||
38: 0x00afd7,
|
||||
39: 0x00afff,
|
||||
40: 0x00d700,
|
||||
41: 0x00d75f,
|
||||
42: 0x00d787,
|
||||
43: 0x00d7af,
|
||||
44: 0x00d7d7,
|
||||
45: 0x00d7ff,
|
||||
46: 0x00ff00,
|
||||
47: 0x00ff5f,
|
||||
48: 0x00ff87,
|
||||
49: 0x00ffaf,
|
||||
50: 0x00ffd7,
|
||||
51: 0x00ffff,
|
||||
52: 0x5f0000,
|
||||
53: 0x5f005f,
|
||||
54: 0x5f0087,
|
||||
55: 0x5f00af,
|
||||
56: 0x5f00d7,
|
||||
57: 0x5f00ff,
|
||||
58: 0x5f5f00,
|
||||
59: 0x5f5f5f,
|
||||
60: 0x5f5f87,
|
||||
61: 0x5f5faf,
|
||||
62: 0x5f5fd7,
|
||||
63: 0x5f5fff,
|
||||
64: 0x5f8700,
|
||||
65: 0x5f875f,
|
||||
66: 0x5f8787,
|
||||
67: 0x5f87af,
|
||||
68: 0x5f87d7,
|
||||
69: 0x5f87ff,
|
||||
70: 0x5faf00,
|
||||
71: 0x5faf5f,
|
||||
72: 0x5faf87,
|
||||
73: 0x5fafaf,
|
||||
74: 0x5fafd7,
|
||||
75: 0x5fafff,
|
||||
76: 0x5fd700,
|
||||
77: 0x5fd75f,
|
||||
78: 0x5fd787,
|
||||
79: 0x5fd7af,
|
||||
80: 0x5fd7d7,
|
||||
81: 0x5fd7ff,
|
||||
82: 0x5fff00,
|
||||
83: 0x5fff5f,
|
||||
84: 0x5fff87,
|
||||
85: 0x5fffaf,
|
||||
86: 0x5fffd7,
|
||||
87: 0x5fffff,
|
||||
88: 0x870000,
|
||||
89: 0x87005f,
|
||||
90: 0x870087,
|
||||
91: 0x8700af,
|
||||
92: 0x8700d7,
|
||||
93: 0x8700ff,
|
||||
94: 0x875f00,
|
||||
95: 0x875f5f,
|
||||
96: 0x875f87,
|
||||
97: 0x875faf,
|
||||
98: 0x875fd7,
|
||||
99: 0x875fff,
|
||||
100: 0x878700,
|
||||
101: 0x87875f,
|
||||
102: 0x878787,
|
||||
103: 0x8787af,
|
||||
104: 0x8787d7,
|
||||
105: 0x8787ff,
|
||||
106: 0x87af00,
|
||||
107: 0x87af5f,
|
||||
108: 0x87af87,
|
||||
109: 0x87afaf,
|
||||
110: 0x87afd7,
|
||||
111: 0x87afff,
|
||||
112: 0x87d700,
|
||||
113: 0x87d75f,
|
||||
114: 0x87d787,
|
||||
115: 0x87d7af,
|
||||
116: 0x87d7d7,
|
||||
117: 0x87d7ff,
|
||||
118: 0x87ff00,
|
||||
119: 0x87ff5f,
|
||||
120: 0x87ff87,
|
||||
121: 0x87ffaf,
|
||||
122: 0x87ffd7,
|
||||
123: 0x87ffff,
|
||||
124: 0xaf0000,
|
||||
125: 0xaf005f,
|
||||
126: 0xaf0087,
|
||||
127: 0xaf00af,
|
||||
128: 0xaf00d7,
|
||||
129: 0xaf00ff,
|
||||
130: 0xaf5f00,
|
||||
131: 0xaf5f5f,
|
||||
132: 0xaf5f87,
|
||||
133: 0xaf5faf,
|
||||
134: 0xaf5fd7,
|
||||
135: 0xaf5fff,
|
||||
136: 0xaf8700,
|
||||
137: 0xaf875f,
|
||||
138: 0xaf8787,
|
||||
139: 0xaf87af,
|
||||
140: 0xaf87d7,
|
||||
141: 0xaf87ff,
|
||||
142: 0xafaf00,
|
||||
143: 0xafaf5f,
|
||||
144: 0xafaf87,
|
||||
145: 0xafafaf,
|
||||
146: 0xafafd7,
|
||||
147: 0xafafff,
|
||||
148: 0xafd700,
|
||||
149: 0xafd75f,
|
||||
150: 0xafd787,
|
||||
151: 0xafd7af,
|
||||
152: 0xafd7d7,
|
||||
153: 0xafd7ff,
|
||||
154: 0xafff00,
|
||||
155: 0xafff5f,
|
||||
156: 0xafff87,
|
||||
157: 0xafffaf,
|
||||
158: 0xafffd7,
|
||||
159: 0xafffff,
|
||||
160: 0xd70000,
|
||||
161: 0xd7005f,
|
||||
162: 0xd70087,
|
||||
163: 0xd700af,
|
||||
164: 0xd700d7,
|
||||
165: 0xd700ff,
|
||||
166: 0xd75f00,
|
||||
167: 0xd75f5f,
|
||||
168: 0xd75f87,
|
||||
169: 0xd75faf,
|
||||
170: 0xd75fd7,
|
||||
171: 0xd75fff,
|
||||
172: 0xd78700,
|
||||
173: 0xd7875f,
|
||||
174: 0xd78787,
|
||||
175: 0xd787af,
|
||||
176: 0xd787d7,
|
||||
177: 0xd787ff,
|
||||
178: 0xd7af00,
|
||||
179: 0xd7af5f,
|
||||
180: 0xd7af87,
|
||||
181: 0xd7afaf,
|
||||
182: 0xd7afd7,
|
||||
183: 0xd7afff,
|
||||
184: 0xd7d700,
|
||||
185: 0xd7d75f,
|
||||
186: 0xd7d787,
|
||||
187: 0xd7d7af,
|
||||
188: 0xd7d7d7,
|
||||
189: 0xd7d7ff,
|
||||
190: 0xd7ff00,
|
||||
191: 0xd7ff5f,
|
||||
192: 0xd7ff87,
|
||||
193: 0xd7ffaf,
|
||||
194: 0xd7ffd7,
|
||||
195: 0xd7ffff,
|
||||
196: 0xff0000,
|
||||
197: 0xff005f,
|
||||
198: 0xff0087,
|
||||
199: 0xff00af,
|
||||
200: 0xff00d7,
|
||||
201: 0xff00ff,
|
||||
202: 0xff5f00,
|
||||
203: 0xff5f5f,
|
||||
204: 0xff5f87,
|
||||
205: 0xff5faf,
|
||||
206: 0xff5fd7,
|
||||
207: 0xff5fff,
|
||||
208: 0xff8700,
|
||||
209: 0xff875f,
|
||||
210: 0xff8787,
|
||||
211: 0xff87af,
|
||||
212: 0xff87d7,
|
||||
213: 0xff87ff,
|
||||
214: 0xffaf00,
|
||||
215: 0xffaf5f,
|
||||
216: 0xffaf87,
|
||||
217: 0xffafaf,
|
||||
218: 0xffafd7,
|
||||
219: 0xffafff,
|
||||
220: 0xffd700,
|
||||
221: 0xffd75f,
|
||||
222: 0xffd787,
|
||||
223: 0xffd7af,
|
||||
224: 0xffd7d7,
|
||||
225: 0xffd7ff,
|
||||
226: 0xffff00,
|
||||
227: 0xffff5f,
|
||||
228: 0xffff87,
|
||||
229: 0xffffaf,
|
||||
230: 0xffffd7,
|
||||
231: 0xffffff,
|
||||
232: 0x080808,
|
||||
233: 0x121212,
|
||||
234: 0x1c1c1c,
|
||||
235: 0x262626,
|
||||
236: 0x303030,
|
||||
237: 0x3a3a3a,
|
||||
238: 0x444444,
|
||||
239: 0x4e4e4e,
|
||||
240: 0x585858,
|
||||
241: 0x626262,
|
||||
242: 0x6c6c6c,
|
||||
243: 0x767676,
|
||||
244: 0x808080,
|
||||
245: 0x8a8a8a,
|
||||
246: 0x949494,
|
||||
247: 0x9e9e9e,
|
||||
248: 0xa8a8a8,
|
||||
249: 0xb2b2b2,
|
||||
250: 0xbcbcbc,
|
||||
251: 0xc6c6c6,
|
||||
252: 0xd0d0d0,
|
||||
253: 0xdadada,
|
||||
254: 0xe4e4e4,
|
||||
255: 0xeeeeee,
|
||||
}
|
||||
|
||||
// `\033]0;TITLESTR\007`
|
||||
func doTitleSequence(er *bytes.Reader) error {
|
||||
var c byte
|
||||
var err error
|
||||
|
||||
c, err = er.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c != '0' && c != '2' {
|
||||
return nil
|
||||
}
|
||||
c, err = er.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c != ';' {
|
||||
return nil
|
||||
}
|
||||
title := make([]byte, 0, 80)
|
||||
for {
|
||||
c, err = er.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c == 0x07 || c == '\n' {
|
||||
break
|
||||
}
|
||||
title = append(title, c)
|
||||
}
|
||||
if len(title) > 0 {
|
||||
title8, err := syscall.UTF16PtrFromString(string(title))
|
||||
if err == nil {
|
||||
procSetConsoleTitle.Call(uintptr(unsafe.Pointer(title8)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write write data on console
|
||||
func (w *Writer) Write(data []byte) (n int, err error) {
|
||||
var csbi consoleScreenBufferInfo
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
|
||||
handle := w.handle
|
||||
|
||||
var er *bytes.Reader
|
||||
if w.rest.Len() > 0 {
|
||||
var rest bytes.Buffer
|
||||
w.rest.WriteTo(&rest)
|
||||
w.rest.Reset()
|
||||
rest.Write(data)
|
||||
er = bytes.NewReader(rest.Bytes())
|
||||
} else {
|
||||
er = bytes.NewReader(data)
|
||||
}
|
||||
var bw [1]byte
|
||||
loop:
|
||||
for {
|
||||
c1, err := er.ReadByte()
|
||||
if err != nil {
|
||||
break loop
|
||||
}
|
||||
if c1 != 0x1b {
|
||||
bw[0] = c1
|
||||
w.out.Write(bw[:])
|
||||
continue
|
||||
}
|
||||
c2, err := er.ReadByte()
|
||||
if err != nil {
|
||||
break loop
|
||||
}
|
||||
|
||||
switch c2 {
|
||||
case ']':
|
||||
w.rest.WriteByte(c1)
|
||||
w.rest.WriteByte(c2)
|
||||
er.WriteTo(&w.rest)
|
||||
if bytes.IndexByte(w.rest.Bytes(), 0x07) == -1 {
|
||||
break loop
|
||||
}
|
||||
er = bytes.NewReader(w.rest.Bytes()[2:])
|
||||
err := doTitleSequence(er)
|
||||
if err != nil {
|
||||
break loop
|
||||
}
|
||||
w.rest.Reset()
|
||||
continue
|
||||
// https://github.com/mattn/go-colorable/issues/27
|
||||
case '7':
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
w.oldpos = csbi.cursorPosition
|
||||
continue
|
||||
case '8':
|
||||
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&w.oldpos)))
|
||||
continue
|
||||
case 0x5b:
|
||||
// execute part after switch
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
w.rest.WriteByte(c1)
|
||||
w.rest.WriteByte(c2)
|
||||
er.WriteTo(&w.rest)
|
||||
|
||||
var buf bytes.Buffer
|
||||
var m byte
|
||||
for i, c := range w.rest.Bytes()[2:] {
|
||||
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
|
||||
m = c
|
||||
er = bytes.NewReader(w.rest.Bytes()[2+i+1:])
|
||||
w.rest.Reset()
|
||||
break
|
||||
}
|
||||
buf.Write([]byte(string(c)))
|
||||
}
|
||||
if m == 0 {
|
||||
break loop
|
||||
}
|
||||
|
||||
switch m {
|
||||
case 'A':
|
||||
n, err = strconv.Atoi(buf.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
csbi.cursorPosition.y -= short(n)
|
||||
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||
case 'B':
|
||||
n, err = strconv.Atoi(buf.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
csbi.cursorPosition.y += short(n)
|
||||
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||
case 'C':
|
||||
n, err = strconv.Atoi(buf.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
csbi.cursorPosition.x += short(n)
|
||||
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||
case 'D':
|
||||
n, err = strconv.Atoi(buf.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
csbi.cursorPosition.x -= short(n)
|
||||
if csbi.cursorPosition.x < 0 {
|
||||
csbi.cursorPosition.x = 0
|
||||
}
|
||||
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||
case 'E':
|
||||
n, err = strconv.Atoi(buf.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
csbi.cursorPosition.x = 0
|
||||
csbi.cursorPosition.y += short(n)
|
||||
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||
case 'F':
|
||||
n, err = strconv.Atoi(buf.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
csbi.cursorPosition.x = 0
|
||||
csbi.cursorPosition.y -= short(n)
|
||||
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||
case 'G':
|
||||
n, err = strconv.Atoi(buf.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
csbi.cursorPosition.x = short(n - 1)
|
||||
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||
case 'H', 'f':
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
if buf.Len() > 0 {
|
||||
token := strings.Split(buf.String(), ";")
|
||||
switch len(token) {
|
||||
case 1:
|
||||
n1, err := strconv.Atoi(token[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
csbi.cursorPosition.y = short(n1 - 1)
|
||||
case 2:
|
||||
n1, err := strconv.Atoi(token[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
n2, err := strconv.Atoi(token[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
csbi.cursorPosition.x = short(n2 - 1)
|
||||
csbi.cursorPosition.y = short(n1 - 1)
|
||||
}
|
||||
} else {
|
||||
csbi.cursorPosition.y = 0
|
||||
}
|
||||
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||
case 'J':
|
||||
n := 0
|
||||
if buf.Len() > 0 {
|
||||
n, err = strconv.Atoi(buf.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
var count, written dword
|
||||
var cursor coord
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
switch n {
|
||||
case 0:
|
||||
cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y}
|
||||
count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.size.y-csbi.cursorPosition.y)*csbi.size.x)
|
||||
case 1:
|
||||
cursor = coord{x: csbi.window.left, y: csbi.window.top}
|
||||
count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.window.top-csbi.cursorPosition.y)*csbi.size.x)
|
||||
case 2:
|
||||
cursor = coord{x: csbi.window.left, y: csbi.window.top}
|
||||
count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.size.y-csbi.cursorPosition.y)*csbi.size.x)
|
||||
}
|
||||
procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
|
||||
procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
|
||||
case 'K':
|
||||
n := 0
|
||||
if buf.Len() > 0 {
|
||||
n, err = strconv.Atoi(buf.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
var cursor coord
|
||||
var count, written dword
|
||||
switch n {
|
||||
case 0:
|
||||
cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y}
|
||||
count = dword(csbi.size.x - csbi.cursorPosition.x)
|
||||
case 1:
|
||||
cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y}
|
||||
count = dword(csbi.size.x - csbi.cursorPosition.x)
|
||||
case 2:
|
||||
cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y}
|
||||
count = dword(csbi.size.x)
|
||||
}
|
||||
procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
|
||||
procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
|
||||
case 'm':
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
attr := csbi.attributes
|
||||
cs := buf.String()
|
||||
if cs == "" {
|
||||
procSetConsoleTextAttribute.Call(uintptr(handle), uintptr(w.oldattr))
|
||||
continue
|
||||
}
|
||||
token := strings.Split(cs, ";")
|
||||
for i := 0; i < len(token); i++ {
|
||||
ns := token[i]
|
||||
if n, err = strconv.Atoi(ns); err == nil {
|
||||
switch {
|
||||
case n == 0 || n == 100:
|
||||
attr = w.oldattr
|
||||
case 1 <= n && n <= 5:
|
||||
attr |= foregroundIntensity
|
||||
case n == 7:
|
||||
attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4)
|
||||
case n == 22 || n == 25:
|
||||
attr |= foregroundIntensity
|
||||
case n == 27:
|
||||
attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4)
|
||||
case 30 <= n && n <= 37:
|
||||
attr &= backgroundMask
|
||||
if (n-30)&1 != 0 {
|
||||
attr |= foregroundRed
|
||||
}
|
||||
if (n-30)&2 != 0 {
|
||||
attr |= foregroundGreen
|
||||
}
|
||||
if (n-30)&4 != 0 {
|
||||
attr |= foregroundBlue
|
||||
}
|
||||
case n == 38: // set foreground color.
|
||||
if i < len(token)-2 && (token[i+1] == "5" || token[i+1] == "05") {
|
||||
if n256, err := strconv.Atoi(token[i+2]); err == nil {
|
||||
if n256foreAttr == nil {
|
||||
n256setup()
|
||||
}
|
||||
attr &= backgroundMask
|
||||
attr |= n256foreAttr[n256]
|
||||
i += 2
|
||||
}
|
||||
} else if len(token) == 5 && token[i+1] == "2" {
|
||||
var r, g, b int
|
||||
r, _ = strconv.Atoi(token[i+2])
|
||||
g, _ = strconv.Atoi(token[i+3])
|
||||
b, _ = strconv.Atoi(token[i+4])
|
||||
i += 4
|
||||
if r > 127 {
|
||||
attr |= foregroundRed
|
||||
}
|
||||
if g > 127 {
|
||||
attr |= foregroundGreen
|
||||
}
|
||||
if b > 127 {
|
||||
attr |= foregroundBlue
|
||||
}
|
||||
} else {
|
||||
attr = attr & (w.oldattr & backgroundMask)
|
||||
}
|
||||
case n == 39: // reset foreground color.
|
||||
attr &= backgroundMask
|
||||
attr |= w.oldattr & foregroundMask
|
||||
case 40 <= n && n <= 47:
|
||||
attr &= foregroundMask
|
||||
if (n-40)&1 != 0 {
|
||||
attr |= backgroundRed
|
||||
}
|
||||
if (n-40)&2 != 0 {
|
||||
attr |= backgroundGreen
|
||||
}
|
||||
if (n-40)&4 != 0 {
|
||||
attr |= backgroundBlue
|
||||
}
|
||||
case n == 48: // set background color.
|
||||
if i < len(token)-2 && token[i+1] == "5" {
|
||||
if n256, err := strconv.Atoi(token[i+2]); err == nil {
|
||||
if n256backAttr == nil {
|
||||
n256setup()
|
||||
}
|
||||
attr &= foregroundMask
|
||||
attr |= n256backAttr[n256]
|
||||
i += 2
|
||||
}
|
||||
} else if len(token) == 5 && token[i+1] == "2" {
|
||||
var r, g, b int
|
||||
r, _ = strconv.Atoi(token[i+2])
|
||||
g, _ = strconv.Atoi(token[i+3])
|
||||
b, _ = strconv.Atoi(token[i+4])
|
||||
i += 4
|
||||
if r > 127 {
|
||||
attr |= backgroundRed
|
||||
}
|
||||
if g > 127 {
|
||||
attr |= backgroundGreen
|
||||
}
|
||||
if b > 127 {
|
||||
attr |= backgroundBlue
|
||||
}
|
||||
} else {
|
||||
attr = attr & (w.oldattr & foregroundMask)
|
||||
}
|
||||
case n == 49: // reset foreground color.
|
||||
attr &= foregroundMask
|
||||
attr |= w.oldattr & backgroundMask
|
||||
case 90 <= n && n <= 97:
|
||||
attr = (attr & backgroundMask)
|
||||
attr |= foregroundIntensity
|
||||
if (n-90)&1 != 0 {
|
||||
attr |= foregroundRed
|
||||
}
|
||||
if (n-90)&2 != 0 {
|
||||
attr |= foregroundGreen
|
||||
}
|
||||
if (n-90)&4 != 0 {
|
||||
attr |= foregroundBlue
|
||||
}
|
||||
case 100 <= n && n <= 107:
|
||||
attr = (attr & foregroundMask)
|
||||
attr |= backgroundIntensity
|
||||
if (n-100)&1 != 0 {
|
||||
attr |= backgroundRed
|
||||
}
|
||||
if (n-100)&2 != 0 {
|
||||
attr |= backgroundGreen
|
||||
}
|
||||
if (n-100)&4 != 0 {
|
||||
attr |= backgroundBlue
|
||||
}
|
||||
}
|
||||
procSetConsoleTextAttribute.Call(uintptr(handle), uintptr(attr))
|
||||
}
|
||||
}
|
||||
case 'h':
|
||||
var ci consoleCursorInfo
|
||||
cs := buf.String()
|
||||
if cs == "5>" {
|
||||
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
|
||||
ci.visible = 0
|
||||
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
|
||||
} else if cs == "?25" {
|
||||
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
|
||||
ci.visible = 1
|
||||
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
|
||||
} else if cs == "?1049" {
|
||||
if w.althandle == 0 {
|
||||
h, _, _ := procCreateConsoleScreenBuffer.Call(uintptr(genericRead|genericWrite), 0, 0, uintptr(consoleTextmodeBuffer), 0, 0)
|
||||
w.althandle = syscall.Handle(h)
|
||||
if w.althandle != 0 {
|
||||
handle = w.althandle
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'l':
|
||||
var ci consoleCursorInfo
|
||||
cs := buf.String()
|
||||
if cs == "5>" {
|
||||
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
|
||||
ci.visible = 1
|
||||
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
|
||||
} else if cs == "?25" {
|
||||
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
|
||||
ci.visible = 0
|
||||
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
|
||||
} else if cs == "?1049" {
|
||||
if w.althandle != 0 {
|
||||
syscall.CloseHandle(w.althandle)
|
||||
w.althandle = 0
|
||||
handle = w.handle
|
||||
}
|
||||
}
|
||||
case 's':
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
w.oldpos = csbi.cursorPosition
|
||||
case 'u':
|
||||
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&w.oldpos)))
|
||||
}
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
type consoleColor struct {
|
||||
rgb int
|
||||
red bool
|
||||
green bool
|
||||
blue bool
|
||||
intensity bool
|
||||
}
|
||||
|
||||
func (c consoleColor) foregroundAttr() (attr word) {
|
||||
if c.red {
|
||||
attr |= foregroundRed
|
||||
}
|
||||
if c.green {
|
||||
attr |= foregroundGreen
|
||||
}
|
||||
if c.blue {
|
||||
attr |= foregroundBlue
|
||||
}
|
||||
if c.intensity {
|
||||
attr |= foregroundIntensity
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c consoleColor) backgroundAttr() (attr word) {
|
||||
if c.red {
|
||||
attr |= backgroundRed
|
||||
}
|
||||
if c.green {
|
||||
attr |= backgroundGreen
|
||||
}
|
||||
if c.blue {
|
||||
attr |= backgroundBlue
|
||||
}
|
||||
if c.intensity {
|
||||
attr |= backgroundIntensity
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var color16 = []consoleColor{
|
||||
{0x000000, false, false, false, false},
|
||||
{0x000080, false, false, true, false},
|
||||
{0x008000, false, true, false, false},
|
||||
{0x008080, false, true, true, false},
|
||||
{0x800000, true, false, false, false},
|
||||
{0x800080, true, false, true, false},
|
||||
{0x808000, true, true, false, false},
|
||||
{0xc0c0c0, true, true, true, false},
|
||||
{0x808080, false, false, false, true},
|
||||
{0x0000ff, false, false, true, true},
|
||||
{0x00ff00, false, true, false, true},
|
||||
{0x00ffff, false, true, true, true},
|
||||
{0xff0000, true, false, false, true},
|
||||
{0xff00ff, true, false, true, true},
|
||||
{0xffff00, true, true, false, true},
|
||||
{0xffffff, true, true, true, true},
|
||||
}
|
||||
|
||||
type hsv struct {
|
||||
h, s, v float32
|
||||
}
|
||||
|
||||
func (a hsv) dist(b hsv) float32 {
|
||||
dh := a.h - b.h
|
||||
switch {
|
||||
case dh > 0.5:
|
||||
dh = 1 - dh
|
||||
case dh < -0.5:
|
||||
dh = -1 - dh
|
||||
}
|
||||
ds := a.s - b.s
|
||||
dv := a.v - b.v
|
||||
return float32(math.Sqrt(float64(dh*dh + ds*ds + dv*dv)))
|
||||
}
|
||||
|
||||
func toHSV(rgb int) hsv {
|
||||
r, g, b := float32((rgb&0xFF0000)>>16)/256.0,
|
||||
float32((rgb&0x00FF00)>>8)/256.0,
|
||||
float32(rgb&0x0000FF)/256.0
|
||||
min, max := minmax3f(r, g, b)
|
||||
h := max - min
|
||||
if h > 0 {
|
||||
if max == r {
|
||||
h = (g - b) / h
|
||||
if h < 0 {
|
||||
h += 6
|
||||
}
|
||||
} else if max == g {
|
||||
h = 2 + (b-r)/h
|
||||
} else {
|
||||
h = 4 + (r-g)/h
|
||||
}
|
||||
}
|
||||
h /= 6.0
|
||||
s := max - min
|
||||
if max != 0 {
|
||||
s /= max
|
||||
}
|
||||
v := max
|
||||
return hsv{h: h, s: s, v: v}
|
||||
}
|
||||
|
||||
type hsvTable []hsv
|
||||
|
||||
func toHSVTable(rgbTable []consoleColor) hsvTable {
|
||||
t := make(hsvTable, len(rgbTable))
|
||||
for i, c := range rgbTable {
|
||||
t[i] = toHSV(c.rgb)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (t hsvTable) find(rgb int) consoleColor {
|
||||
hsv := toHSV(rgb)
|
||||
n := 7
|
||||
l := float32(5.0)
|
||||
for i, p := range t {
|
||||
d := hsv.dist(p)
|
||||
if d < l {
|
||||
l, n = d, i
|
||||
}
|
||||
}
|
||||
return color16[n]
|
||||
}
|
||||
|
||||
func minmax3f(a, b, c float32) (min, max float32) {
|
||||
if a < b {
|
||||
if b < c {
|
||||
return a, c
|
||||
} else if a < c {
|
||||
return a, b
|
||||
} else {
|
||||
return c, b
|
||||
}
|
||||
} else {
|
||||
if a < c {
|
||||
return b, c
|
||||
} else if b < c {
|
||||
return b, a
|
||||
} else {
|
||||
return c, a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var n256foreAttr []word
|
||||
var n256backAttr []word
|
||||
|
||||
func n256setup() {
|
||||
n256foreAttr = make([]word, 256)
|
||||
n256backAttr = make([]word, 256)
|
||||
t := toHSVTable(color16)
|
||||
for i, rgb := range color256 {
|
||||
c := t.find(rgb)
|
||||
n256foreAttr[i] = c.foregroundAttr()
|
||||
n256backAttr[i] = c.backgroundAttr()
|
||||
}
|
||||
}
|
||||
55
vendor/github.com/mattn/go-colorable/noncolorable.go
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
package colorable
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
// NonColorable hold writer but remove escape sequence.
|
||||
type NonColorable struct {
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
// NewNonColorable return new instance of Writer which remove escape sequence from Writer.
|
||||
func NewNonColorable(w io.Writer) io.Writer {
|
||||
return &NonColorable{out: w}
|
||||
}
|
||||
|
||||
// Write write data on console
|
||||
func (w *NonColorable) Write(data []byte) (n int, err error) {
|
||||
er := bytes.NewReader(data)
|
||||
var bw [1]byte
|
||||
loop:
|
||||
for {
|
||||
c1, err := er.ReadByte()
|
||||
if err != nil {
|
||||
break loop
|
||||
}
|
||||
if c1 != 0x1b {
|
||||
bw[0] = c1
|
||||
w.out.Write(bw[:])
|
||||
continue
|
||||
}
|
||||
c2, err := er.ReadByte()
|
||||
if err != nil {
|
||||
break loop
|
||||
}
|
||||
if c2 != 0x5b {
|
||||
continue
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
for {
|
||||
c, err := er.ReadByte()
|
||||
if err != nil {
|
||||
break loop
|
||||
}
|
||||
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
|
||||
break
|
||||
}
|
||||
buf.Write([]byte(string(c)))
|
||||
}
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||