Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
30
.assets/cluster-mysql.dot
Normal file
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
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
BIN
.assets/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 179 KiB |
65
.assets/flow-diagram.dot
Normal file
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
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
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
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 |
390
.assets/sql-schema.svg
Normal file
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 |
@@ -1,11 +1,12 @@
|
||||
defaults: &defaults
|
||||
working_directory: /go/src/github.com/moul/sshportal
|
||||
docker:
|
||||
- image: circleci/golang:1.8
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.8
|
||||
# - image: circleci/mysql:9.4
|
||||
|
||||
working_directory: /go/src/github.com/moul/sshportal
|
||||
go.build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- run: make install
|
||||
@@ -14,4 +15,26 @@ jobs:
|
||||
# - run: make integration
|
||||
- run: go get -u github.com/alecthomas/gometalinter
|
||||
- run: gometalinter --install
|
||||
- run: make lint
|
||||
- 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
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
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
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/log/
|
||||
/sshportal
|
||||
*.db
|
||||
/data
|
||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,5 +1,36 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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
|
||||
|
||||
12
Makefile
12
Makefile
@@ -3,7 +3,6 @@ GIT_TAG ?= $(shell git describe --tags --always)
|
||||
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
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)
|
||||
PORT ?= 2222
|
||||
AES_KEY ?= my-dummy-aes-key
|
||||
|
||||
.PHONY: install
|
||||
@@ -16,7 +15,7 @@ docker.build:
|
||||
|
||||
.PHONY: integration
|
||||
integration:
|
||||
PORT="$(PORT)" bash ./examples/integration/test.sh
|
||||
cd ./examples/integration && make
|
||||
|
||||
.PHONY: _docker_install
|
||||
_docker_install:
|
||||
@@ -25,7 +24,7 @@ _docker_install:
|
||||
.PHONY: dev
|
||||
dev:
|
||||
-go get github.com/githubnemo/CompileDaemon
|
||||
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal --debug --bind-address=:$(PORT) --aes-key=$(AES_KEY) $(EXTRA_RUN_OPTS)" .
|
||||
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal server --debug --bind-address=:$(PORT) --aes-key=$(AES_KEY) $(EXTRA_RUN_OPTS)" .
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@@ -34,9 +33,14 @@ test:
|
||||
|
||||
.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=20s .
|
||||
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
|
||||
|
||||
153
README.md
153
README.md
@@ -2,9 +2,11 @@
|
||||
|
||||
[](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
|
||||
|
||||
@@ -12,50 +14,57 @@ 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
|
||||
* User Roles
|
||||
* User invitations
|
||||
* Easy authorized_keys installation
|
||||
* 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
|
||||
* Audit log
|
||||
* 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
|
||||
* 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
|
||||
```
|
||||
@@ -121,14 +130,20 @@ 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
|
||||
|
||||

|
||||
|
||||
## built-in shell
|
||||
|
||||
`sshportal` embeds a configuration CLI.
|
||||
|
||||
@@ -165,11 +180,11 @@ event inspect [-h] EVENT...
|
||||
|
||||
# host management
|
||||
host help
|
||||
host create [-h] [--name=<value>] [--password=<value>] [--comment=<value>] [--key=KEY] [--group=HOSTGROUP...] <username>[:<password>]@<host>[:<port>]
|
||||
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...] 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
|
||||
@@ -181,6 +196,7 @@ hostgroup rm [-h] HOSTGROUP...
|
||||
# key management
|
||||
key help
|
||||
key create [-h] [--name=<value>] [--type=<value>] [--length=<value>] [--comment=<value>]
|
||||
key import [-h] [--name=<value>] [--comment=<value>]
|
||||
key inspect [-h] [--decrypt] KEY...
|
||||
key ls [-h] [--latest] [--quiet]
|
||||
key rm [-h] KEY...
|
||||
@@ -223,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.6.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
|
||||
@@ -232,7 +248,7 @@ docker logs -f sshportal
|
||||
The easier way to upgrade sshportal is to do the following:
|
||||
|
||||
```sh
|
||||
# we consider you were using an old version and you want to use the new version v1.6.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
|
||||
@@ -240,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.6.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
|
||||
```
|
||||
@@ -340,3 +356,62 @@ $
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
2
acl.go
2
acl.go
@@ -30,7 +30,7 @@ func CheckACLs(user User, host Host) (string, error) {
|
||||
}
|
||||
|
||||
// transform map to slice and sort it
|
||||
acls := []*ACL{}
|
||||
acls := make([]*ACL, 0, len(aclMap))
|
||||
for _, acl := range aclMap {
|
||||
acls = append(acls, acl)
|
||||
}
|
||||
|
||||
49
config.go
Normal file
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
|
||||
}
|
||||
51
crypto.go
51
crypto.go
@@ -9,6 +9,7 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
@@ -52,6 +53,42 @@ 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)
|
||||
@@ -95,17 +132,14 @@ func safeDecrypt(key []byte, cryptoText string) string {
|
||||
return out
|
||||
}
|
||||
|
||||
func HostEncrypt(aesKey string, host *Host) error {
|
||||
func HostEncrypt(aesKey string, host *Host) (err error) {
|
||||
if aesKey == "" {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
if host.Password != "" {
|
||||
if host.Password, err = encrypt([]byte(aesKey), host.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
host.Password, err = encrypt([]byte(aesKey), host.Password)
|
||||
}
|
||||
return nil
|
||||
return
|
||||
}
|
||||
func HostDecrypt(aesKey string, host *Host) {
|
||||
if aesKey == "" {
|
||||
@@ -116,13 +150,12 @@ func HostDecrypt(aesKey string, host *Host) {
|
||||
}
|
||||
}
|
||||
|
||||
func SSHKeyEncrypt(aesKey string, key *SSHKey) error {
|
||||
func SSHKeyEncrypt(aesKey string, key *SSHKey) (err error) {
|
||||
if aesKey == "" {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
key.PrivKey, err = encrypt([]byte(aesKey), key.PrivKey)
|
||||
return err
|
||||
return
|
||||
}
|
||||
func SSHKeyDecrypt(aesKey string, key *SSHKey) {
|
||||
if aesKey == "" {
|
||||
|
||||
198
db.go
198
db.go
@@ -1,4 +1,3 @@
|
||||
//go:generate stringer -type=SessionStatus
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -7,12 +6,13 @@ import (
|
||||
"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 {
|
||||
@@ -54,14 +54,17 @@ 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"`
|
||||
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
|
||||
@@ -157,6 +160,13 @@ const (
|
||||
ACLActionDeny = "deny"
|
||||
)
|
||||
|
||||
type BastionScheme string
|
||||
|
||||
const (
|
||||
BastionSchemeSSH BastionScheme = "ssh"
|
||||
BastionSchemeTelnet = "telnet"
|
||||
)
|
||||
|
||||
func init() {
|
||||
unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*")
|
||||
|
||||
@@ -169,53 +179,150 @@ 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 ""
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Host helpers
|
||||
|
||||
func HostsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Groups").Preload("SSHKey")
|
||||
}
|
||||
func HostsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
||||
}
|
||||
func HostByName(db *gorm.DB, name string) (*Host, error) {
|
||||
var host Host
|
||||
db.Preload("SSHKey").Where("name = ?", name).Find(&host)
|
||||
if host.Name == "" {
|
||||
// FIXME: add available hosts
|
||||
return nil, fmt.Errorf("No such target: %q", name)
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
func (host *Host) clientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, error) {
|
||||
config := gossh.ClientConfig{
|
||||
User: host.Username(),
|
||||
HostKeyCallback: hk,
|
||||
Auth: []gossh.AuthMethod{},
|
||||
}
|
||||
if host.SSHKey != nil {
|
||||
signer, err := gossh.ParsePrivateKey([]byte(host.SSHKey.PrivKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Auth = append(config.Auth, gossh.PublicKeys(signer))
|
||||
}
|
||||
if host.Passwd() != "" {
|
||||
config.Auth = append(config.Auth, gossh.Password(host.Passwd()))
|
||||
}
|
||||
if len(config.Auth) == 0 {
|
||||
return nil, fmt.Errorf("no valid authentication method for host %q", host.Name)
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// SSHKey helpers
|
||||
|
||||
@@ -252,25 +359,20 @@ func UsersPreload(db *gorm.DB) *gorm.DB {
|
||||
func UsersByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers).Or("email IN (?)", identifiers).Or("name IN (?)", identifiers)
|
||||
}
|
||||
func UserHasRole(user User, name string) bool {
|
||||
for _, role := range user.Roles {
|
||||
func (u *User) HasRole(name string) bool {
|
||||
for _, role := range u.Roles {
|
||||
if role.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func UserCheckRoles(user User, names []string) error {
|
||||
ok := false
|
||||
func (u *User) CheckRoles(names []string) error {
|
||||
for _, name := range names {
|
||||
if UserHasRole(user, name) {
|
||||
ok = true
|
||||
break
|
||||
if u.HasRole(name) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("you don't have permission to access this feature (requires any of these roles: '%s')", strings.Join(names, "', '"))
|
||||
}
|
||||
|
||||
|
||||
75
dbinit.go
75
dbinit.go
@@ -5,6 +5,8 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate"
|
||||
@@ -434,6 +436,52 @@ func dbInit(db *gorm.DB) error {
|
||||
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 {
|
||||
@@ -511,7 +559,9 @@ 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)
|
||||
@@ -522,9 +572,20 @@ func dbInit(db *gorm.DB) error {
|
||||
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: "admin",
|
||||
Email: "admin@sshportal",
|
||||
Name: username,
|
||||
Email: fmt.Sprintf("%s@localhost", username),
|
||||
Comment: "created by sshportal",
|
||||
Roles: []*UserRole{&adminRole},
|
||||
InviteToken: inviteToken,
|
||||
@@ -553,14 +614,10 @@ func dbInit(db *gorm.DB) error {
|
||||
}
|
||||
|
||||
// close unclosed connections
|
||||
if err := db.Table("sessions").Where("status = ?", "active").Updates(&Session{
|
||||
return db.Table("sessions").Where("status = ?", "active").Updates(&Session{
|
||||
Status: SessionStatusClosed,
|
||||
ErrMsg: "sshportal was halted while the connection was still active",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}).Error
|
||||
}
|
||||
|
||||
func hardDeleteCallback(scope *gorm.Scope) {
|
||||
|
||||
24
examples/homebrew/sshportal.rb
Normal file
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
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
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
|
||||
75
examples/integration/_client.sh
Executable file
75
examples/integration/_client.sh
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/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
|
||||
)
|
||||
|
||||
# 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"]}'
|
||||
|
||||
# TODO: test more cases (forwards, scp, sftp, interactive, pty, stdin, exit code, ...)
|
||||
27
examples/integration/client_test_rsa
Normal file
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
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,87 +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
|
||||
(
|
||||
cat backup-1 | grep -v '"date":' > backup-1.clean
|
||||
cat backup-2 | grep -v '"date":' > backup-2.clean
|
||||
set -xe
|
||||
diff backup-1.clean backup-2.clean
|
||||
)
|
||||
|
||||
# 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
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
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)
|
||||
}
|
||||
322
main.go
322
main.go
@@ -1,13 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
@@ -15,12 +14,11 @@ 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.6.0"
|
||||
Version = "1.8.0"
|
||||
// GitTag will be overwritten automatically by the build system
|
||||
GitTag string
|
||||
// GitSha will be overwritten automatically by the build system
|
||||
@@ -29,14 +27,6 @@ var (
|
||||
GitBranch string
|
||||
)
|
||||
|
||||
type sshportalContextKey string
|
||||
|
||||
var (
|
||||
userContextKey = sshportalContextKey("user")
|
||||
messageContextKey = sshportalContextKey("message")
|
||||
errorContextKey = sshportalContextKey("error")
|
||||
)
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
@@ -45,230 +35,128 @@ func main() {
|
||||
app.Author = "Manfred Touron"
|
||||
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.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",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "healthcheck-user",
|
||||
Usage: "SSH user that returns healthcheck status without checking the SSH key",
|
||||
Value: "healthcheck",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "aes-key",
|
||||
Usage: "Encrypt sensitive data in database (length: 16, 24 or 32)",
|
||||
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 {
|
||||
switch len(c.String("aes-key")) {
|
||||
case 0, 16, 24, 32:
|
||||
default:
|
||||
return fmt.Errorf("invalid aes key size, should be 16 or 24, 32")
|
||||
}
|
||||
// db
|
||||
db, err := gorm.Open(c.String("db-driver"), c.String("db-conn"))
|
||||
if err != nil {
|
||||
return err
|
||||
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() {
|
||||
if err2 := db.Close(); err2 != nil {
|
||||
panic(err2)
|
||||
origErr := err
|
||||
err = db.Close()
|
||||
if origErr != nil {
|
||||
err = origErr
|
||||
}
|
||||
}()
|
||||
if err = db.DB().Ping(); err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
if c.Bool("debug") {
|
||||
db.LogMode(true)
|
||||
db.LogMode(c.debug)
|
||||
if err = dbInit(db); err != nil {
|
||||
return
|
||||
}
|
||||
if err := dbInit(db); err != nil {
|
||||
|
||||
// create TCP listening socket
|
||||
ln, err := net.Listen("tcp", c.bindAddr)
|
||||
if 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)
|
||||
// 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 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("healthcheck-user"):
|
||||
fmt.Fprintln(s, "OK")
|
||||
return
|
||||
case username == currentUser.Name || username == currentUser.Email || username == c.String("config-user"):
|
||||
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 err2 := db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", currentUser.ID).First(&tmpUser).Error; err2 != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err2)
|
||||
return
|
||||
}
|
||||
var tmpHost Host
|
||||
if err2 := db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", host.ID).First(&tmpHost).Error; err2 != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err2)
|
||||
return
|
||||
}
|
||||
|
||||
action, err2 := CheckACLs(tmpUser, tmpHost)
|
||||
if err2 != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err2)
|
||||
return
|
||||
}
|
||||
|
||||
// decrypt key and password
|
||||
HostDecrypt(c.String("aes-key"), host)
|
||||
SSHKeyDecrypt(c.String("aes-key"), host.SSHKey)
|
||||
|
||||
switch action {
|
||||
case ACLActionAllow:
|
||||
sess := Session{
|
||||
UserID: currentUser.ID,
|
||||
HostID: host.ID,
|
||||
Status: SessionStatusActive,
|
||||
}
|
||||
if err2 := db.Create(&sess).Error; err2 != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err2)
|
||||
return
|
||||
}
|
||||
sessUpdate := Session{}
|
||||
if err2 := proxy(s, host, DynamicHostKey(db, host)); err2 != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err2)
|
||||
sessUpdate.ErrMsg = fmt.Sprintf("%v", err2)
|
||||
switch sessUpdate.ErrMsg {
|
||||
case "lch closed the connection", "rch closed the connection":
|
||||
sessUpdate.ErrMsg = ""
|
||||
}
|
||||
}
|
||||
sessUpdate.Status = SessionStatusClosed
|
||||
now := time.Now()
|
||||
sessUpdate.StoppedAt = &now
|
||||
db.Model(&sess).Updates(&sessUpdate)
|
||||
case ACLActionDeny:
|
||||
fmt.Fprintf(s, "You don't have permission to that host.\n")
|
||||
default:
|
||||
fmt.Fprintf(s, "error: invalid ACL action: %q\n", action)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
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("authorized_key = ?", string(gossh.MarshalAuthorizedKey(key))).First(&userKey)
|
||||
if userKey.UserID > 0 {
|
||||
db.Preload("Roles").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",
|
||||
AuthorizedKey: string(gossh.MarshalAuthorizedKey(key)),
|
||||
}
|
||||
db.Create(&userKey)
|
||||
|
||||
// token is only usable once
|
||||
user.InviteToken = ""
|
||||
db.Model(&user).Updates(&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 {
|
||||
var key SSHKey
|
||||
if err := SSHKeysByIdentifiers(db, []string{"host"}).First(&key).Error; 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
|
||||
}
|
||||
SSHKeyDecrypt(c.String("aes-key"), &key)
|
||||
}
|
||||
|
||||
signer, err := gossh.ParsePrivateKey([]byte(key.PrivKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.AddHostKey(signer)
|
||||
return nil
|
||||
})
|
||||
|
||||
log.Printf("info: 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
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
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()
|
||||
}
|
||||
102
proxy.go
102
proxy.go
@@ -1,102 +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, hk gossh.HostKeyCallback) error {
|
||||
config, err := host.clientConfig(s, hk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rconn, err := gossh.Dial("tcp", host.Addr, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = rconn.Close() }()
|
||||
|
||||
rch, rreqs, err := rconn.OpenChannel("session", []byte{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("SSH Connection 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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (host *Host) clientConfig(_ ssh.Session, hk gossh.HostKeyCallback) (*gossh.ClientConfig, error) {
|
||||
config := gossh.ClientConfig{
|
||||
User: host.User,
|
||||
HostKeyCallback: hk,
|
||||
Auth: []gossh.AuthMethod{},
|
||||
}
|
||||
if host.SSHKey != nil {
|
||||
signer, err := gossh.ParsePrivateKey([]byte(host.SSHKey.PrivKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Auth = append(config.Auth, gossh.PublicKeys(signer))
|
||||
}
|
||||
if host.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
|
||||
}
|
||||
348
shell.go
348
shell.go
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/asaskevich/govalidator"
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/mgutz/ansi"
|
||||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
@@ -33,7 +32,15 @@ var banner = `
|
||||
`
|
||||
var startTime = time.Now()
|
||||
|
||||
func shell(globalContext *cli.Context, s ssh.Session, sshCommand []string, db *gorm.DB) error {
|
||||
const (
|
||||
naMessage = "n/a"
|
||||
)
|
||||
|
||||
func shell(s ssh.Session) error {
|
||||
var (
|
||||
sshCommand = s.Command()
|
||||
actx = s.Context().Value(authContextKey).(*authContext)
|
||||
)
|
||||
if len(sshCommand) == 0 {
|
||||
if _, err := fmt.Fprint(s, banner); err != nil {
|
||||
return err
|
||||
@@ -55,7 +62,11 @@ GLOBAL OPTIONS:
|
||||
app.Writer = s
|
||||
app.HideVersion = true
|
||||
|
||||
myself := s.Context().Value(userContextKey).(User)
|
||||
var (
|
||||
myself = &actx.user
|
||||
db = actx.db
|
||||
)
|
||||
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "acl",
|
||||
@@ -67,14 +78,14 @@ GLOBAL OPTIONS:
|
||||
Description: "$> acl create -",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringSliceFlag{Name: "hostgroup, hg", Usage: "Assigns `HOSTGROUPS` to the acl"},
|
||||
cli.StringSliceFlag{Name: "usergroup, ug", Usage: "Assigns `HOSTGROUPS` to the acl"},
|
||||
cli.StringSliceFlag{Name: "usergroup, ug", Usage: "Assigns `USERGROUP` to the acl"},
|
||||
cli.StringFlag{Name: "pattern", Usage: "Assigns a host pattern to the acl"},
|
||||
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
||||
cli.StringFlag{Name: "action", Usage: "Assigns the ACL action (allow,deny)", Value: ACLActionAllow},
|
||||
cli.UintFlag{Name: "weight, w", Usage: "Assigns the ACL weight (priority)"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
acl := ACL{
|
||||
@@ -124,7 +135,7 @@ GLOBAL OPTIONS:
|
||||
if c.NArg() < 1 {
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -145,7 +156,7 @@ GLOBAL OPTIONS:
|
||||
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -206,7 +217,7 @@ GLOBAL OPTIONS:
|
||||
if c.NArg() < 1 {
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -230,7 +241,7 @@ GLOBAL OPTIONS:
|
||||
if c.NArg() < 1 {
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -264,10 +275,16 @@ GLOBAL OPTIONS:
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := model.Association("UserGroups").Append(&appendUserGroups).Delete(deleteUserGroups).Error; err != nil {
|
||||
if err := model.Association("UserGroups").Append(&appendUserGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if len(deleteUserGroups) > 0 {
|
||||
if err := model.Association("UserGroups").Delete(deleteUserGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var appendHostGroups []HostGroup
|
||||
var deleteHostGroups []HostGroup
|
||||
@@ -279,10 +296,16 @@ GLOBAL OPTIONS:
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := model.Association("HostGroups").Append(&appendHostGroups).Delete(deleteHostGroups).Error; err != nil {
|
||||
if err := model.Association("HostGroups").Append(&appendHostGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if len(deleteHostGroups) > 0 {
|
||||
if err := model.Association("HostGroups").Delete(deleteHostGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
@@ -299,10 +322,11 @@ GLOBAL OPTIONS:
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{Name: "indent", Usage: "uses indented JSON"},
|
||||
cli.BoolFlag{Name: "decrypt", Usage: "decrypt sensitive data"},
|
||||
cli.BoolFlag{Name: "ignore-events", Usage: "do not backup events data"},
|
||||
},
|
||||
Description: "ssh admin@portal config backup > sshportal.bkp",
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -315,11 +339,11 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
for _, key := range config.SSHKeys {
|
||||
SSHKeyDecrypt(globalContext.String("aes-key"), key)
|
||||
SSHKeyDecrypt(actx.config.aesKey, key)
|
||||
}
|
||||
if !c.Bool("decrypt") {
|
||||
for _, key := range config.SSHKeys {
|
||||
if err := SSHKeyEncrypt(globalContext.String("aes-key"), key); err != nil {
|
||||
if err := SSHKeyEncrypt(actx.config.aesKey, key); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -329,11 +353,11 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
for _, host := range config.Hosts {
|
||||
HostDecrypt(globalContext.String("aes-key"), host)
|
||||
HostDecrypt(actx.config.aesKey, host)
|
||||
}
|
||||
if !c.Bool("decrypt") {
|
||||
for _, host := range config.Hosts {
|
||||
if err := HostEncrypt(globalContext.String("aes-key"), host); err != nil {
|
||||
if err := HostEncrypt(actx.config.aesKey, host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -360,8 +384,10 @@ GLOBAL OPTIONS:
|
||||
if err := SessionsPreload(db).Find(&config.Sessions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := EventsPreload(db).Find(&config.Events).Error; err != nil {
|
||||
return err
|
||||
if !c.Bool("ignore-events") {
|
||||
if err := EventsPreload(db).Find(&config.Events).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
config.Date = time.Now()
|
||||
enc := json.NewEncoder(s)
|
||||
@@ -379,7 +405,7 @@ GLOBAL OPTIONS:
|
||||
cli.BoolFlag{Name: "decrypt", Usage: "do not encrypt sensitive data"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -446,9 +472,9 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
}
|
||||
for _, host := range config.Hosts {
|
||||
HostDecrypt(globalContext.String("aes-key"), host)
|
||||
HostDecrypt(actx.config.aesKey, host)
|
||||
if !c.Bool("decrypt") {
|
||||
if err := HostEncrypt(globalContext.String("aes-key"), host); err != nil {
|
||||
if err := HostEncrypt(actx.config.aesKey, host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -482,9 +508,9 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
}
|
||||
for _, sshKey := range config.SSHKeys {
|
||||
SSHKeyDecrypt(globalContext.String("aes-key"), sshKey)
|
||||
SSHKeyDecrypt(actx.config.aesKey, sshKey)
|
||||
if !c.Bool("decrypt") {
|
||||
if err := SSHKeyEncrypt(globalContext.String("aes-key"), sshKey); err != nil {
|
||||
if err := SSHKeyEncrypt(actx.config.aesKey, sshKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -540,7 +566,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -569,7 +595,7 @@ GLOBAL OPTIONS:
|
||||
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -625,13 +651,14 @@ GLOBAL OPTIONS:
|
||||
{
|
||||
Name: "create",
|
||||
Usage: "Creates a new host",
|
||||
ArgsUsage: "<user>[:<password>]@<host>[:<port>]",
|
||||
ArgsUsage: "[scheme://]<user>[:<password>]@<host>[:<port>]",
|
||||
Description: "$> host create bart@foo.org\n $> host create bob:marley@example.com:2222",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{Name: "name, n", Usage: "Assigns a name to the host"},
|
||||
cli.StringFlag{Name: "password, p", Usage: "If present, sshportal will use password-based authentication"},
|
||||
cli.StringFlag{Name: "comment, c"},
|
||||
cli.StringFlag{Name: "key, k", Usage: "`KEY` to use for authentication"},
|
||||
cli.StringFlag{Name: "hop, o", Usage: "Hop to use for connecting to the server"},
|
||||
cli.StringSliceFlag{Name: "group, g", Usage: "Assigns the host to `HOSTGROUPS` (default: \"default\")"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
@@ -639,24 +666,33 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, err := NewHostFromURL(c.Args().First())
|
||||
u, err := ParseInputURL(c.Args().First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
host := &Host{
|
||||
URL: u.String(),
|
||||
Comment: c.String("comment"),
|
||||
}
|
||||
if c.String("password") != "" {
|
||||
host.Password = c.String("password")
|
||||
}
|
||||
host.Name = strings.Split(host.Hostname(), ".")[0]
|
||||
|
||||
if c.String("hop") != "" {
|
||||
hop, err := HostByName(db, c.String("hop"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
host.Hop = hop
|
||||
}
|
||||
if c.String("name") != "" {
|
||||
host.Name = c.String("name")
|
||||
}
|
||||
// FIXME: check if name already exists
|
||||
host.Comment = c.String("comment")
|
||||
|
||||
if _, err := govalidator.ValidateStruct(host); err != nil {
|
||||
return err
|
||||
@@ -684,7 +720,7 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
|
||||
// encrypt
|
||||
if err := HostEncrypt(globalContext.String("aes-key"), host); err != nil {
|
||||
if err := HostEncrypt(actx.config.aesKey, host); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -706,13 +742,13 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin", "listhosts"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin", "listhosts"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var hosts []*Host
|
||||
db = db.Preload("Groups")
|
||||
if UserHasRole(myself, "admin") {
|
||||
if myself.HasRole("admin") {
|
||||
db = db.Preload("SSHKey")
|
||||
}
|
||||
if err := HostsByIdentifiers(db, c.Args()).Find(&hosts).Error; err != nil {
|
||||
@@ -721,7 +757,7 @@ GLOBAL OPTIONS:
|
||||
|
||||
if c.Bool("decrypt") {
|
||||
for _, host := range hosts {
|
||||
HostDecrypt(globalContext.String("aes-keuy"), host)
|
||||
HostDecrypt(actx.config.aesKey, host)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,7 +773,7 @@ GLOBAL OPTIONS:
|
||||
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin", "listhosts"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin", "listhosts"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -763,14 +799,11 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(s)
|
||||
table.SetHeader([]string{"ID", "Name", "URL", "Key", "Pass", "Groups", "Updated", "Created", "Comment"})
|
||||
table.SetHeader([]string{"ID", "Name", "URL", "Key", "Groups", "Updated", "Created", "Comment", "Hop"})
|
||||
table.SetBorder(false)
|
||||
table.SetCaption(true, fmt.Sprintf("Total: %d hosts.", len(hosts)))
|
||||
for _, host := range hosts {
|
||||
authKey, authPass := "", ""
|
||||
if host.Password != "" {
|
||||
authPass = "yes"
|
||||
}
|
||||
authKey := ""
|
||||
if host.SSHKeyID > 0 {
|
||||
var key SSHKey
|
||||
db.Model(&host).Related(&key)
|
||||
@@ -780,16 +813,24 @@ GLOBAL OPTIONS:
|
||||
for _, hostGroup := range host.Groups {
|
||||
groupNames = append(groupNames, hostGroup.Name)
|
||||
}
|
||||
var hop string
|
||||
if host.HopID != 0 {
|
||||
var hopHost Host
|
||||
db.Model(&host).Related(&hopHost, "HopID")
|
||||
hop = hopHost.Name
|
||||
} else {
|
||||
hop = ""
|
||||
}
|
||||
table.Append([]string{
|
||||
fmt.Sprintf("%d", host.ID),
|
||||
host.Name,
|
||||
host.URL(),
|
||||
host.String(),
|
||||
authKey,
|
||||
authPass,
|
||||
strings.Join(groupNames, ", "),
|
||||
humanize.Time(host.UpdatedAt),
|
||||
humanize.Time(host.CreatedAt),
|
||||
host.Comment,
|
||||
hop,
|
||||
//FIXME: add some stats about last access time etc
|
||||
})
|
||||
}
|
||||
@@ -805,7 +846,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -817,9 +858,11 @@ GLOBAL OPTIONS:
|
||||
ArgsUsage: "HOST...",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{Name: "name, n", Usage: "Rename the host"},
|
||||
cli.StringFlag{Name: "password, p", Usage: "Update/set a password, use \"none\" to unset"},
|
||||
cli.StringFlag{Name: "url, u", Usage: "Update connection URL"},
|
||||
cli.StringFlag{Name: "comment, c", Usage: "Update/set a host comment"},
|
||||
cli.StringFlag{Name: "key, k", Usage: "Link a `KEY` to use for authentication"},
|
||||
cli.StringFlag{Name: "hop, o", Usage: "Change the hop to use for connecting to the server"},
|
||||
cli.BoolFlag{Name: "unset-hop", Usage: "Remove the hop set for this host"},
|
||||
cli.StringSliceFlag{Name: "assign-group, g", Usage: "Assign the host to a new `HOSTGROUPS`"},
|
||||
cli.StringSliceFlag{Name: "unassign-group", Usage: "Unassign the host from a `HOSTGROUPS`"},
|
||||
},
|
||||
@@ -828,7 +871,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -845,7 +888,7 @@ GLOBAL OPTIONS:
|
||||
for _, host := range hosts {
|
||||
model := tx.Model(&host)
|
||||
// simple fields
|
||||
for _, fieldname := range []string{"name", "comment", "password"} {
|
||||
for _, fieldname := range []string{"name", "comment"} {
|
||||
if c.String(fieldname) != "" {
|
||||
if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil {
|
||||
tx.Rollback()
|
||||
@@ -854,6 +897,42 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
}
|
||||
|
||||
// url
|
||||
if c.String("url") != "" {
|
||||
u, err := ParseInputURL(c.String("url"))
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := model.Update("url", u.String()).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// hop
|
||||
if c.String("hop") != "" {
|
||||
hop, err := HostByName(db, c.String("hop"))
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := model.Association("Hop").Replace(hop).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// remove the hop
|
||||
if c.Bool("unset-hop") {
|
||||
var hopHost Host
|
||||
db.Model(&host).Related(&hopHost, "HopID")
|
||||
if err := model.Association("Hop").Delete(hopHost).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// associations
|
||||
if c.String("key") != "" {
|
||||
var key SSHKey
|
||||
@@ -876,10 +955,16 @@ GLOBAL OPTIONS:
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := model.Association("Groups").Append(&appendGroups).Delete(deleteGroups).Error; err != nil {
|
||||
if err := model.Association("Groups").Append(&appendGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if len(deleteGroups) > 0 {
|
||||
if err := model.Association("Groups").Delete(deleteGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
@@ -899,7 +984,7 @@ GLOBAL OPTIONS:
|
||||
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -930,7 +1015,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -951,7 +1036,7 @@ GLOBAL OPTIONS:
|
||||
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1004,7 +1089,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1016,18 +1101,18 @@ GLOBAL OPTIONS:
|
||||
Name: "info",
|
||||
Usage: "Shows system-wide information",
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(s, "Debug mode (server): %v\n", globalContext.Bool("debug"))
|
||||
fmt.Fprintf(s, "debug mode (server): %v\n", actx.config.debug)
|
||||
hostname, _ := os.Hostname()
|
||||
fmt.Fprintf(s, "Hostname: %s\n", hostname)
|
||||
fmt.Fprintf(s, "CPUs: %d\n", runtime.NumCPU())
|
||||
fmt.Fprintf(s, "Demo mode: %v\n", globalContext.Bool("demo"))
|
||||
fmt.Fprintf(s, "DB Driver: %s\n", globalContext.String("db-driver"))
|
||||
fmt.Fprintf(s, "DB Conn: %s\n", globalContext.String("db-conn"))
|
||||
fmt.Fprintf(s, "Bind Address: %s\n", globalContext.String("bind-address"))
|
||||
fmt.Fprintf(s, "Demo mode: %v\n", actx.config.demo)
|
||||
fmt.Fprintf(s, "DB Driver: %s\n", actx.config.dbDriver)
|
||||
fmt.Fprintf(s, "DB Conn: %s\n", actx.config.dbURL)
|
||||
fmt.Fprintf(s, "Bind Address: %s\n", actx.config.bindAddr)
|
||||
fmt.Fprintf(s, "System Time: %v\n", time.Now().Format(time.RFC3339Nano))
|
||||
fmt.Fprintf(s, "OS Type: %s\n", runtime.GOOS)
|
||||
fmt.Fprintf(s, "OS Architecture: %s\n", runtime.GOARCH)
|
||||
@@ -1035,7 +1120,7 @@ GLOBAL OPTIONS:
|
||||
fmt.Fprintf(s, "Go version (build): %v\n", runtime.Version())
|
||||
fmt.Fprintf(s, "Uptime: %v\n", time.Since(startTime))
|
||||
|
||||
fmt.Fprintf(s, "User email: %v\n", myself.ID)
|
||||
fmt.Fprintf(s, "User ID: %v\n", myself.ID)
|
||||
fmt.Fprintf(s, "User email: %s\n", myself.Email)
|
||||
fmt.Fprintf(s, "Version: %s\n", Version)
|
||||
fmt.Fprintf(s, "GIT SHA: %s\n", GitSha)
|
||||
@@ -1063,7 +1148,7 @@ GLOBAL OPTIONS:
|
||||
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1073,8 +1158,8 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
|
||||
key, err := NewSSHKey(c.String("type"), c.Uint("length"))
|
||||
if globalContext.String("aes-key") != "" {
|
||||
if err2 := SSHKeyEncrypt(globalContext.String("aes-key"), key); err2 != nil {
|
||||
if actx.config.aesKey != "" {
|
||||
if err2 := SSHKeyEncrypt(actx.config.aesKey, key); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
}
|
||||
@@ -1097,6 +1182,60 @@ GLOBAL OPTIONS:
|
||||
return nil
|
||||
},
|
||||
}, {
|
||||
Name: "import",
|
||||
Usage: "Imports an existing private key",
|
||||
Description: "$> key import\n $> key import --name=mykey",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{Name: "name", Usage: "Assigns a name to the key"},
|
||||
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var name string
|
||||
if c.String("name") != "" {
|
||||
name = c.String("name")
|
||||
} else {
|
||||
name = namesgenerator.GetRandomName(0)
|
||||
}
|
||||
|
||||
var value string
|
||||
term := terminal.NewTerminal(s, "Paste your key and end with a blank line> ")
|
||||
for {
|
||||
line, err := term.ReadLine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if line != "" {
|
||||
value += line + "\n"
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
key, err := ImportSSHKey(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key.Name = name
|
||||
key.Comment = c.String("comment")
|
||||
|
||||
if _, err := govalidator.ValidateStruct(key); err != nil {
|
||||
return err
|
||||
}
|
||||
// FIXME: check if name already exists
|
||||
|
||||
// save the key in database
|
||||
if err := db.Create(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(s, "%d\n", key.ID)
|
||||
|
||||
return nil
|
||||
},
|
||||
}, {
|
||||
Name: "inspect",
|
||||
Usage: "Shows detailed information on one or more keys",
|
||||
ArgsUsage: "KEY...",
|
||||
@@ -1108,7 +1247,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1119,7 +1258,7 @@ GLOBAL OPTIONS:
|
||||
|
||||
if c.Bool("decrypt") {
|
||||
for _, key := range keys {
|
||||
SSHKeyDecrypt(globalContext.String("aes-key"), key)
|
||||
SSHKeyDecrypt(actx.config.aesKey, key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1135,7 +1274,7 @@ GLOBAL OPTIONS:
|
||||
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1189,7 +1328,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1228,7 +1367,7 @@ GLOBAL OPTIONS:
|
||||
if err := SSHKeysByIdentifiers(SSHKeysPreload(db), c.Args()).First(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
SSHKeyDecrypt(globalContext.String("aes-key"), &key)
|
||||
SSHKeyDecrypt(actx.config.aesKey, &key)
|
||||
|
||||
type line struct {
|
||||
key string
|
||||
@@ -1302,7 +1441,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1330,7 +1469,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1366,7 +1505,7 @@ GLOBAL OPTIONS:
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(s, "User %d created.\nTo associate this account with a key, use the following SSH user: 'invite-%s'.\n", user.ID, user.InviteToken)
|
||||
fmt.Fprintf(s, "User %d created.\nTo associate this account with a key, use the following SSH user: 'invite:%s'.\n", user.ID, user.InviteToken)
|
||||
return nil
|
||||
},
|
||||
}, {
|
||||
@@ -1377,7 +1516,7 @@ GLOBAL OPTIONS:
|
||||
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1438,7 +1577,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1461,7 +1600,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1503,11 +1642,16 @@ GLOBAL OPTIONS:
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := model.Association("Groups").Append(&appendGroups).Delete(deleteGroups).Error; err != nil {
|
||||
if err := model.Association("Groups").Append(&appendGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if len(deleteGroups) > 0 {
|
||||
if err := model.Association("Groups").Delete(deleteGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
var appendRoles []UserRole
|
||||
if err := UserRolesByIdentifiers(db, c.StringSlice("assign-role")).Find(&appendRoles).Error; err != nil {
|
||||
tx.Rollback()
|
||||
@@ -1518,12 +1662,17 @@ GLOBAL OPTIONS:
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := model.Association("Roles").Append(&appendRoles).Delete(deleteRoles).Error; err != nil {
|
||||
if err := model.Association("Roles").Append(&appendRoles).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if len(deleteRoles) > 0 {
|
||||
if err := model.Association("Roles").Delete(deleteRoles).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
},
|
||||
},
|
||||
@@ -1541,7 +1690,7 @@ GLOBAL OPTIONS:
|
||||
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1559,7 +1708,7 @@ GLOBAL OPTIONS:
|
||||
// FIXME: check if name already exists
|
||||
// FIXME: add myself to the new group
|
||||
|
||||
userGroup.Users = []*User{&myself}
|
||||
userGroup.Users = []*User{myself}
|
||||
|
||||
if err := db.Create(&userGroup).Error; err != nil {
|
||||
return err
|
||||
@@ -1576,7 +1725,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1597,7 +1746,7 @@ GLOBAL OPTIONS:
|
||||
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1649,7 +1798,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1674,7 +1823,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1721,7 +1870,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1742,7 +1891,7 @@ GLOBAL OPTIONS:
|
||||
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1771,9 +1920,13 @@ GLOBAL OPTIONS:
|
||||
table.SetBorder(false)
|
||||
table.SetCaption(true, fmt.Sprintf("Total: %d userkeys.", len(userKeys)))
|
||||
for _, userkey := range userKeys {
|
||||
email := naMessage
|
||||
if userkey.User != nil {
|
||||
email = userkey.User.Email
|
||||
}
|
||||
table.Append([]string{
|
||||
fmt.Sprintf("%d", userkey.ID),
|
||||
userkey.User.Email,
|
||||
email,
|
||||
// FIXME: add fingerprint
|
||||
humanize.Time(userkey.UpdatedAt),
|
||||
humanize.Time(userkey.CreatedAt),
|
||||
@@ -1792,7 +1945,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1813,7 +1966,7 @@ GLOBAL OPTIONS:
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1834,7 +1987,7 @@ GLOBAL OPTIONS:
|
||||
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1870,10 +2023,18 @@ GLOBAL OPTIONS:
|
||||
duration = humanize.RelTime(session.CreatedAt, *session.StoppedAt, "", "")
|
||||
}
|
||||
duration = strings.Replace(duration, "now", "1 second", 1)
|
||||
hostname := naMessage
|
||||
if session.Host != nil {
|
||||
hostname = session.Host.Name
|
||||
}
|
||||
username := naMessage
|
||||
if session.User != nil {
|
||||
username = session.User.Name
|
||||
}
|
||||
table.Append([]string{
|
||||
fmt.Sprintf("%d", session.ID),
|
||||
session.User.Name,
|
||||
session.Host.Name,
|
||||
username,
|
||||
hostname,
|
||||
session.Status,
|
||||
humanize.Time(session.CreatedAt),
|
||||
duration,
|
||||
@@ -1918,7 +2079,10 @@ GLOBAL OPTIONS:
|
||||
if len(words) == 1 && strings.ToLower(words[0]) == "exit" {
|
||||
return s.Exit(0)
|
||||
}
|
||||
NewEvent("shell", words[0]).SetAuthor(&myself).SetArg("interactive", true).SetArg("args", words[1:]).Log(db)
|
||||
if len(words) == 0 {
|
||||
continue
|
||||
}
|
||||
NewEvent("shell", words[0]).SetAuthor(myself).SetArg("interactive", true).SetArg("args", words[1:]).Log(db)
|
||||
if err := app.Run(append([]string{"config"}, words...)); err != nil {
|
||||
if cliErr, ok := err.(*cli.ExitError); ok {
|
||||
if cliErr.ExitCode() != 0 {
|
||||
@@ -1931,7 +2095,7 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
}
|
||||
} else { // oneshot mode
|
||||
NewEvent("shell", sshCommand[0]).SetAuthor(&myself).SetArg("interactive", false).SetArg("args", sshCommand[1:]).Log(db)
|
||||
NewEvent("shell", sshCommand[0]).SetAuthor(myself).SetArg("interactive", false).SetArg("args", sshCommand[1:]).Log(db)
|
||||
if err := app.Run(append([]string{"config"}, sshCommand...)); err != nil {
|
||||
if errMsg := err.Error(); errMsg != "" {
|
||||
fmt.Fprintf(s, "error: %s\n", errMsg)
|
||||
|
||||
346
ssh.go
346
ssh.go
@@ -2,35 +2,349 @@ 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 dynamicHostKey struct {
|
||||
db *gorm.DB
|
||||
host *Host
|
||||
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
|
||||
}
|
||||
|
||||
func (d *dynamicHostKey) check(hostname string, remote net.Addr, key gossh.PublicKey) error {
|
||||
if len(d.host.HostKey) == 0 {
|
||||
log.Println("Discovering host fingerprint...")
|
||||
return d.db.Model(d.host).Update("HostKey", key.Marshal()).Error
|
||||
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
|
||||
}
|
||||
|
||||
if !bytes.Equal(d.host.HostKey, key.Marshal()) {
|
||||
return fmt.Errorf("ssh: host key mismatch")
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DynamicHostKey returns a function for use in
|
||||
// ClientConfig.HostKeyCallback to dynamically learn or accept host key.
|
||||
func DynamicHostKey(db *gorm.DB, host *Host) gossh.HostKeyCallback {
|
||||
// FIXME: forward interactively the host key checking
|
||||
hk := &dynamicHostKey{db, host}
|
||||
return hk.check
|
||||
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
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
22
vendor/github.com/arkan/bastion/LICENSE
generated
vendored
Normal file
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
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()
|
||||
}
|
||||
2
vendor/github.com/gliderlabs/ssh/README.md
generated
vendored
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
|
||||
)
|
||||
19
vendor/github.com/reiver/go-oi/LICENSE
generated
vendored
Normal file
19
vendor/github.com/reiver/go-oi/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2016 Charles Iliya Krempeaux <charles@reptile.ca> :: http://changelog.ca/
|
||||
|
||||
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.
|
||||
64
vendor/github.com/reiver/go-oi/README.md
generated
vendored
Normal file
64
vendor/github.com/reiver/go-oi/README.md
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
# go-oi
|
||||
|
||||
Package **oi** provides useful tools to be used with the Go programming language's standard "io" package.
|
||||
|
||||
For example, did you know that when you call the `Write` method on something that fits the `io.Writer`
|
||||
interface, that it is possible that not everything was be written?!
|
||||
|
||||
I.e., that a _**short write**_ happened.
|
||||
|
||||
That just doing the following is (in general) **not** enough:
|
||||
```
|
||||
n, err := writer.Write(p)
|
||||
```
|
||||
|
||||
That, for example, you should be checking if `err == io.ErrShortWrite`, and then maybe calling the `Write`
|
||||
method again but only with what didn't get written.
|
||||
|
||||
For a simple example of this (that actually is **not** sufficient to solve this problem, but illustrates
|
||||
the direction you would need to go to solve this problem is):
|
||||
```
|
||||
n, err := w.Write(p)
|
||||
|
||||
if io.ErrShortWrite == err {
|
||||
n2, err2 := w.Write(p[n:])
|
||||
}
|
||||
```
|
||||
|
||||
Note that the second call to the `Write` method passed `p[n:]` (instead of just `p`), to account for the `n` bytes
|
||||
already being written (with the first call to the `Write` method).
|
||||
|
||||
A more "production quality" version of this would likely be in a loop, but such that that the loop had "guards"
|
||||
against looping forever, and also possibly looping for "too long".
|
||||
|
||||
Well package **oi** provides tools that helps you deal with this and other problems. For example, you
|
||||
can handle a _**short write**_ with the following **oi** func:
|
||||
```
|
||||
n, err := oi.LongWrite(writer, p)
|
||||
```
|
||||
|
||||
|
||||
## Documention
|
||||
|
||||
Online documentation, which includes examples, can be found at: http://godoc.org/github.com/reiver/go-oi
|
||||
|
||||
[](https://godoc.org/github.com/reiver/go-oi)
|
||||
|
||||
|
||||
## Example
|
||||
```
|
||||
import (
|
||||
"github.com/reiver/go-oi"
|
||||
)
|
||||
|
||||
// ...
|
||||
|
||||
p := []byte("It is important that this message be written!!!")
|
||||
|
||||
n, err := oi.LongWrite(writer, p)
|
||||
if nil != err {
|
||||
//@TODO: Handle error.
|
||||
return
|
||||
}
|
||||
|
||||
```
|
||||
39
vendor/github.com/reiver/go-oi/doc.go
generated
vendored
Normal file
39
vendor/github.com/reiver/go-oi/doc.go
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Package oi provides useful tools to be used with Go's standard "io" package.
|
||||
|
||||
For example, did you know that when you call the Write method on something that fits the io.Writer
|
||||
interface, that it is possible that not everything was be written?!
|
||||
|
||||
I.e., that a 'short write' happened.
|
||||
|
||||
That just doing the following is (in general) not enough:
|
||||
|
||||
n, err := writer.Write(p)
|
||||
|
||||
That, for example, you should be checking if "err == io.ErrShortWrite", and then maybe calling the Write
|
||||
method again but only with what didn't get written.
|
||||
|
||||
For a simple example of this (that actually is not sufficient to solve this problem, but illustrates
|
||||
the direction you would need to go to solve this problem is):
|
||||
|
||||
n, err := w.Write(p)
|
||||
|
||||
if io.ErrShortWrite == err {
|
||||
n2, err2 := w.Write(p[n:])
|
||||
}
|
||||
|
||||
Note that the second call to the Write method passed "p[n:]" (instead of just "p"), to account for the "n" bytes
|
||||
already being written (with the first call to the `Write` method).
|
||||
|
||||
A more "production quality" version of this would likely be in a loop, but such that that the loop had "guards"
|
||||
against looping forever, and also possibly looping for "too long".
|
||||
|
||||
|
||||
Well package oi provides tools that helps you deal with this and other problems. For example, you
|
||||
can handle a 'short write' with the following oi func:
|
||||
```
|
||||
n, err := oi.LongWrite(writer, p)
|
||||
```
|
||||
|
||||
*/
|
||||
package oi
|
||||
39
vendor/github.com/reiver/go-oi/longwrite.go
generated
vendored
Normal file
39
vendor/github.com/reiver/go-oi/longwrite.go
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
package oi
|
||||
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
|
||||
// LongWrite tries to write the bytes from 'p' to the writer 'w', such that it deals
|
||||
// with "short writes" where w.Write would return an error of io.ErrShortWrite and
|
||||
// n < len(p).
|
||||
//
|
||||
// Note that LongWrite still could return the error io.ErrShortWrite; but this
|
||||
// would only be after trying to handle the io.ErrShortWrite a number of times, and
|
||||
// then eventually giving up.
|
||||
func LongWrite(w io.Writer, p []byte) (int64, error) {
|
||||
|
||||
numWritten := int64(0)
|
||||
for {
|
||||
//@TODO: Should check to make sure this doesn't get stuck in an infinite loop writting nothing!
|
||||
n, err := w.Write(p)
|
||||
numWritten += int64(n)
|
||||
if nil != err && io.ErrShortWrite != err {
|
||||
return numWritten, err
|
||||
}
|
||||
|
||||
if !(n < len(p)) {
|
||||
break
|
||||
}
|
||||
|
||||
p = p[n:]
|
||||
|
||||
if len(p) < 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return numWritten, nil
|
||||
}
|
||||
28
vendor/github.com/reiver/go-oi/longwritebyte.go
generated
vendored
Normal file
28
vendor/github.com/reiver/go-oi/longwritebyte.go
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
package oi
|
||||
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
|
||||
// LongWriteByte trys to write the byte from 'b' to the writer 'w', such that it deals
|
||||
// with "short writes" where w.Write would return an error of io.ErrShortWrite and
|
||||
// n < 1.
|
||||
//
|
||||
// Note that LongWriteByte still could return the error io.ErrShortWrite; but this
|
||||
// would only be after trying to handle the io.ErrShortWrite a number of times, and
|
||||
// then eventually giving up.
|
||||
func LongWriteByte(w io.Writer, b byte) error {
|
||||
var buffer [1]byte
|
||||
p := buffer[:]
|
||||
|
||||
buffer[0] = b
|
||||
|
||||
numWritten, err := LongWrite(w, p)
|
||||
if 1 != numWritten {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
39
vendor/github.com/reiver/go-oi/longwritestring.go
generated
vendored
Normal file
39
vendor/github.com/reiver/go-oi/longwritestring.go
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
package oi
|
||||
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
|
||||
// LongWriteString tries to write the bytes from 's' to the writer 'w', such that it deals
|
||||
// with "short writes" where w.Write (or w.WriteString) would return an error of io.ErrShortWrite
|
||||
// and n < len(s).
|
||||
//
|
||||
// Note that LongWriteString still could return the error io.ErrShortWrite; but this
|
||||
// would only be after trying to handle the io.ErrShortWrite a number of times, and
|
||||
// then eventually giving up.
|
||||
func LongWriteString(w io.Writer, s string) (int64, error) {
|
||||
|
||||
numWritten := int64(0)
|
||||
for {
|
||||
//@TODO: Should check to make sure this doesn't get stuck in an infinite loop writting nothing!
|
||||
n, err := io.WriteString(w, s)
|
||||
numWritten += int64(n)
|
||||
if nil != err && io.ErrShortWrite != err {
|
||||
return numWritten, err
|
||||
}
|
||||
|
||||
if !(n < len(s)) {
|
||||
break
|
||||
}
|
||||
|
||||
s = s[n:]
|
||||
|
||||
if len(s) < 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return numWritten, nil
|
||||
}
|
||||
38
vendor/github.com/reiver/go-oi/writenopcloser.go
generated
vendored
Normal file
38
vendor/github.com/reiver/go-oi/writenopcloser.go
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
package oi
|
||||
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
|
||||
// WriteNopCloser takes an io.Writer and returns an io.WriteCloser where
|
||||
// calling the Write method on the returned io.WriterCloser calls the
|
||||
// Write method on the io.Writer it received, but whre calling the Close
|
||||
// method on the returned io.WriterCloser does "nothing" (i.e., is a "nop").
|
||||
//
|
||||
// This is useful in cases where an io.WriteCloser is expected, but you
|
||||
// only have an io.Writer (where closing doesn't make sense) and you
|
||||
// need to make your io.Writer fit. (I.e., you need an adaptor.)
|
||||
func WriteNopCloser(w io.Writer) io.WriteCloser {
|
||||
wc := internalWriteNopCloser{
|
||||
writer:w,
|
||||
}
|
||||
|
||||
return &wc
|
||||
}
|
||||
|
||||
|
||||
type internalWriteNopCloser struct {
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
|
||||
func (wc * internalWriteNopCloser) Write(p []byte) (n int, err error) {
|
||||
return wc.writer.Write(p)
|
||||
}
|
||||
|
||||
|
||||
func (wc * internalWriteNopCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
19
vendor/github.com/reiver/go-telnet/LICENSE
generated
vendored
Normal file
19
vendor/github.com/reiver/go-telnet/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2016 Charles Iliya Krempeaux <charles@reptile.ca> :: http://changelog.ca/
|
||||
|
||||
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.
|
||||
252
vendor/github.com/reiver/go-telnet/README.md
generated
vendored
Normal file
252
vendor/github.com/reiver/go-telnet/README.md
generated
vendored
Normal file
@@ -0,0 +1,252 @@
|
||||
# go-telnet
|
||||
|
||||
Package **telnet** provides TELNET and TELNETS client and server implementations, for the Go programming language.
|
||||
|
||||
|
||||
The **telnet** package provides an API in a style similar to the "net/http" library that is part of the Go standard library, including support for "middleware".
|
||||
|
||||
|
||||
(TELNETS is *secure TELNET*, with the TELNET protocol over a secured TLS (or SSL) connection.)
|
||||
|
||||
|
||||
## Documention
|
||||
|
||||
Online documentation, which includes examples, can be found at: http://godoc.org/github.com/reiver/go-telnet
|
||||
|
||||
[](https://godoc.org/github.com/reiver/go-telnet)
|
||||
|
||||
|
||||
## Very Simple TELNET Server Example
|
||||
|
||||
A very very simple TELNET server is shown in the following code.
|
||||
|
||||
This particular TELNET server just echos back to the user anything they "submit" to the server.
|
||||
|
||||
(By default, a TELNET client does *not* send anything to the server until the [Enter] key is pressed.
|
||||
"Submit" means typing something and then pressing the [Enter] key.)
|
||||
|
||||
```
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/reiver/go-telnet"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var handler telnet.Handler = telnet.EchoHandler
|
||||
|
||||
err := telnet.ListenAndServe(":5555", handler)
|
||||
if nil != err {
|
||||
//@TODO: Handle this error better.
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
If you wanted to test out this very very simple TELNET server, if you were on the same computer it was
|
||||
running, you could connect to it using the bash command:
|
||||
```
|
||||
telnet localhost 5555
|
||||
```
|
||||
(Note that we use the same TCP port number -- "5555" -- as we had in our code. That is important, as the
|
||||
value used by your TELNET server and the value used by your TELNET client **must** match.)
|
||||
|
||||
|
||||
## Very Simple (Secure) TELNETS Server Example
|
||||
|
||||
TELNETS is the secure version of TELNET.
|
||||
|
||||
The code to make a TELNETS server is very similar to the code to make a TELNET server.
|
||||
(The difference between we use the `telnet.ListenAndServeTLS` func instead of the
|
||||
`telnet.ListenAndServe` func.)
|
||||
|
||||
```
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/reiver/go-telnet"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var handler telnet.Handler = telnet.EchoHandler
|
||||
|
||||
err := telnet.ListenAndServeTLS(":5555", "cert.pem", "key.pem", handler)
|
||||
if nil != err {
|
||||
//@TODO: Handle this error better.
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
If you wanted to test out this very very simple TELNETS server, get the `telnets` client program from here:
|
||||
https://github.com/reiver/telnets
|
||||
|
||||
|
||||
## TELNET Client Example:
|
||||
```
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/reiver/go-telnet"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var caller Caller = telnet.StandardCaller
|
||||
|
||||
//@TOOD: replace "example.net:5555" with address you want to connect to.
|
||||
telnet.DialToAndCall("example.net:5555", caller)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## TELNETS Client Example:
|
||||
```
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/reiver/go-telnet"
|
||||
|
||||
"crypto/tls"
|
||||
)
|
||||
|
||||
func main() {
|
||||
//@TODO: Configure the TLS connection here, if you need to.
|
||||
tlsConfig := &tls.Config{}
|
||||
|
||||
var caller Caller = telnet.StandardCaller
|
||||
|
||||
//@TOOD: replace "example.net:5555" with address you want to connect to.
|
||||
telnet.DialToAndCallTLS("example.net:5555", caller, tlsConfig)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## TELNET Shell Server Example
|
||||
|
||||
A more useful TELNET servers can be made using the `"github.com/reiver/go-telnet/telsh"` sub-package.
|
||||
|
||||
For example:
|
||||
```
|
||||
package main
|
||||
|
||||
|
||||
import (
|
||||
"github.com/reiver/go-oi"
|
||||
"github.com/reiver/go-telnet"
|
||||
"github.com/reiver/go-telnet/telsh"
|
||||
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
||||
func fiveHandler(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser)error {
|
||||
oi.LongWriteString(stdout, "The number FIVE looks like this: 5\r\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fiveProducer(ctx telsh.Context, name string, args ...string) telsh.Handler{
|
||||
return telsh.PromoteHandlerFunc(fiveHandler)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func danceHandler(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser)error {
|
||||
for i:=0; i<20; i++ {
|
||||
oi.LongWriteString(stdout, "\r⠋")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠙")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠹")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠸")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠼")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠴")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠦")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠧")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠇")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠏")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
}
|
||||
oi.LongWriteString(stdout, "\r \r\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func danceProducer(ctx telsh.Context, name string, args ...string) telsh.Handler{
|
||||
|
||||
return telsh.PromoteHandlerFunc(danceHandler)
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
|
||||
shellHandler := telsh.NewShellHandler()
|
||||
|
||||
shellHandler.WelcomeMessage = `
|
||||
__ __ ______ _ _____ ____ __ __ ______
|
||||
\ \ / /| ____|| | / ____| / __ \ | \/ || ____|
|
||||
\ \ /\ / / | |__ | | | | | | | || \ / || |__
|
||||
\ \/ \/ / | __| | | | | | | | || |\/| || __|
|
||||
\ /\ / | |____ | |____ | |____ | |__| || | | || |____
|
||||
\/ \/ |______||______| \_____| \____/ |_| |_||______|
|
||||
|
||||
`
|
||||
|
||||
|
||||
// Register the "five" command.
|
||||
commandName := "five"
|
||||
commandProducer := telsh.ProducerFunc(fiveProducer)
|
||||
|
||||
shellHandler.Register(commandName, commandProducer)
|
||||
|
||||
|
||||
|
||||
// Register the "dance" command.
|
||||
commandName = "dance"
|
||||
commandProducer = telsh.ProducerFunc(danceProducer)
|
||||
|
||||
shellHandler.Register(commandName, commandProducer)
|
||||
|
||||
|
||||
|
||||
shellHandler.Register("dance", telsh.ProducerFunc(danceProducer))
|
||||
|
||||
addr := ":5555"
|
||||
if err := telnet.ListenAndServe(addr, shellHandler); nil != err {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
TELNET servers made using the `"github.com/reiver/go-telnet/telsh"` sub-package will often be more useful
|
||||
as it makes it easier for you to create a *shell* interface.
|
||||
|
||||
|
||||
# More Information
|
||||
|
||||
There is a lot more information about documentation on all this here: http://godoc.org/github.com/reiver/go-telnet
|
||||
|
||||
(You should really read those.)
|
||||
|
||||
18
vendor/github.com/reiver/go-telnet/caller.go
generated
vendored
Normal file
18
vendor/github.com/reiver/go-telnet/caller.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package telnet
|
||||
|
||||
|
||||
// A Caller represents the client end of a TELNET (or TELNETS) connection.
|
||||
//
|
||||
// Writing data to the Writer passed as an argument to the CallTELNET method
|
||||
// will send data to the TELNET (or TELNETS) server.
|
||||
//
|
||||
// Reading data from the Reader passed as an argument to the CallTELNET method
|
||||
// will receive data from the TELNET server.
|
||||
//
|
||||
// The Writer's Write method sends "escaped" TELNET (and TELNETS) data.
|
||||
//
|
||||
// The Reader's Read method "un-escapes" TELNET (and TELNETS) data, and filters
|
||||
// out TELNET (and TELNETS) command sequences.
|
||||
type Caller interface {
|
||||
CallTELNET(Context, Writer, Reader)
|
||||
}
|
||||
100
vendor/github.com/reiver/go-telnet/client.go
generated
vendored
Normal file
100
vendor/github.com/reiver/go-telnet/client.go
generated
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
package telnet
|
||||
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
)
|
||||
|
||||
|
||||
func DialAndCall(caller Caller) error {
|
||||
conn, err := Dial()
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &Client{Caller:caller}
|
||||
|
||||
return client.Call(conn)
|
||||
}
|
||||
|
||||
|
||||
func DialToAndCall(srvAddr string, caller Caller) error {
|
||||
conn, err := DialTo(srvAddr)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &Client{Caller:caller}
|
||||
|
||||
return client.Call(conn)
|
||||
}
|
||||
|
||||
|
||||
func DialAndCallTLS(caller Caller, tlsConfig *tls.Config) error {
|
||||
conn, err := DialTLS(tlsConfig)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &Client{Caller:caller}
|
||||
|
||||
return client.Call(conn)
|
||||
}
|
||||
|
||||
func DialToAndCallTLS(srvAddr string, caller Caller, tlsConfig *tls.Config) error {
|
||||
conn, err := DialToTLS(srvAddr, tlsConfig)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &Client{Caller:caller}
|
||||
|
||||
return client.Call(conn)
|
||||
}
|
||||
|
||||
|
||||
type Client struct {
|
||||
Caller Caller
|
||||
|
||||
Logger Logger
|
||||
}
|
||||
|
||||
|
||||
func (client *Client) Call(conn *Conn) error {
|
||||
|
||||
logger := client.logger()
|
||||
|
||||
|
||||
caller := client.Caller
|
||||
if nil == caller {
|
||||
logger.Debug("Defaulted caller to StandardCaller.")
|
||||
caller = StandardCaller
|
||||
}
|
||||
|
||||
|
||||
var ctx Context = NewContext().InjectLogger(logger)
|
||||
|
||||
var w Writer = conn
|
||||
var r Reader = conn
|
||||
|
||||
caller.CallTELNET(ctx, w, r)
|
||||
conn.Close()
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (client *Client) logger() Logger {
|
||||
logger := client.Logger
|
||||
if nil == logger {
|
||||
logger = internalDiscardLogger{}
|
||||
}
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
|
||||
func (client *Client) SetAuth(username string) {
|
||||
//@TODO: #################################################
|
||||
}
|
||||
148
vendor/github.com/reiver/go-telnet/conn.go
generated
vendored
Normal file
148
vendor/github.com/reiver/go-telnet/conn.go
generated
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
package telnet
|
||||
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
)
|
||||
|
||||
|
||||
type Conn struct {
|
||||
conn interface {
|
||||
Read(b []byte) (n int, err error)
|
||||
Write(b []byte) (n int, err error)
|
||||
Close() error
|
||||
LocalAddr() net.Addr
|
||||
RemoteAddr() net.Addr
|
||||
}
|
||||
dataReader *internalDataReader
|
||||
dataWriter *internalDataWriter
|
||||
}
|
||||
|
||||
|
||||
// Dial makes a (un-secure) TELNET client connection to the system's 'loopback address'
|
||||
// (also known as "localhost" or 127.0.0.1).
|
||||
//
|
||||
// If a secure connection is desired, use `DialTLS` instead.
|
||||
func Dial() (*Conn, error) {
|
||||
return DialTo("")
|
||||
}
|
||||
|
||||
// DialTo makes a (un-secure) TELNET client connection to the the address specified by
|
||||
// 'addr'.
|
||||
//
|
||||
// If a secure connection is desired, use `DialToTLS` instead.
|
||||
func DialTo(addr string) (*Conn, error) {
|
||||
|
||||
const network = "tcp"
|
||||
|
||||
if "" == addr {
|
||||
addr = "127.0.0.1:telnet"
|
||||
}
|
||||
|
||||
conn, err := net.Dial(network, addr)
|
||||
if nil != err {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataReader := newDataReader(conn)
|
||||
dataWriter := newDataWriter(conn)
|
||||
|
||||
clientConn := Conn{
|
||||
conn:conn,
|
||||
dataReader:dataReader,
|
||||
dataWriter:dataWriter,
|
||||
}
|
||||
|
||||
return &clientConn, nil
|
||||
}
|
||||
|
||||
|
||||
// DialTLS makes a (secure) TELNETS client connection to the system's 'loopback address'
|
||||
// (also known as "localhost" or 127.0.0.1).
|
||||
func DialTLS(tlsConfig *tls.Config) (*Conn, error) {
|
||||
return DialToTLS("", tlsConfig)
|
||||
}
|
||||
|
||||
// DialToTLS makes a (secure) TELNETS client connection to the the address specified by
|
||||
// 'addr'.
|
||||
func DialToTLS(addr string, tlsConfig *tls.Config) (*Conn, error) {
|
||||
|
||||
const network = "tcp"
|
||||
|
||||
if "" == addr {
|
||||
addr = "127.0.0.1:telnets"
|
||||
}
|
||||
|
||||
conn, err := tls.Dial(network, addr, tlsConfig)
|
||||
if nil != err {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataReader := newDataReader(conn)
|
||||
dataWriter := newDataWriter(conn)
|
||||
|
||||
clientConn := Conn{
|
||||
conn:conn,
|
||||
dataReader:dataReader,
|
||||
dataWriter:dataWriter,
|
||||
}
|
||||
|
||||
return &clientConn, nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Close closes the client connection.
|
||||
//
|
||||
// Typical usage might look like:
|
||||
//
|
||||
// telnetsClient, err = telnet.DialToTLS(addr, tlsConfig)
|
||||
// if nil != err {
|
||||
// //@TODO: Handle error.
|
||||
// return err
|
||||
// }
|
||||
// defer telnetsClient.Close()
|
||||
func (clientConn *Conn) Close() error {
|
||||
return clientConn.conn.Close()
|
||||
}
|
||||
|
||||
|
||||
// Read receives `n` bytes sent from the server to the client,
|
||||
// and "returns" into `p`.
|
||||
//
|
||||
// Note that Read can only be used for receiving TELNET (and TELNETS) data from the server.
|
||||
//
|
||||
// TELNET (and TELNETS) command codes cannot be received using this method, as Read deals
|
||||
// with TELNET (and TELNETS) "unescaping", and (when appropriate) filters out TELNET (and TELNETS)
|
||||
// command codes.
|
||||
//
|
||||
// Read makes Client fit the io.Reader interface.
|
||||
func (clientConn *Conn) Read(p []byte) (n int, err error) {
|
||||
return clientConn.dataReader.Read(p)
|
||||
}
|
||||
|
||||
|
||||
// Write sends `n` bytes from 'p' to the server.
|
||||
//
|
||||
// Note that Write can only be used for sending TELNET (and TELNETS) data to the server.
|
||||
//
|
||||
// TELNET (and TELNETS) command codes cannot be sent using this method, as Write deals with
|
||||
// TELNET (and TELNETS) "escaping", and will properly "escape" anything written with it.
|
||||
//
|
||||
// Write makes Conn fit the io.Writer interface.
|
||||
func (clientConn *Conn) Write(p []byte) (n int, err error) {
|
||||
return clientConn.dataWriter.Write(p)
|
||||
}
|
||||
|
||||
|
||||
// LocalAddr returns the local network address.
|
||||
func (clientConn *Conn) LocalAddr() net.Addr {
|
||||
return clientConn.conn.LocalAddr()
|
||||
}
|
||||
|
||||
|
||||
// RemoteAddr returns the remote network address.
|
||||
func (clientConn *Conn) RemoteAddr() net.Addr {
|
||||
return clientConn.conn.RemoteAddr()
|
||||
}
|
||||
31
vendor/github.com/reiver/go-telnet/context.go
generated
vendored
Normal file
31
vendor/github.com/reiver/go-telnet/context.go
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
package telnet
|
||||
|
||||
|
||||
type Context interface {
|
||||
Logger() Logger
|
||||
|
||||
InjectLogger(Logger) Context
|
||||
}
|
||||
|
||||
|
||||
type internalContext struct {
|
||||
logger Logger
|
||||
}
|
||||
|
||||
|
||||
func NewContext() Context {
|
||||
ctx := internalContext{}
|
||||
|
||||
return &ctx
|
||||
}
|
||||
|
||||
|
||||
func (ctx *internalContext) Logger() Logger {
|
||||
return ctx.logger
|
||||
}
|
||||
|
||||
func (ctx *internalContext) InjectLogger(logger Logger) Context {
|
||||
ctx.logger = logger
|
||||
|
||||
return ctx
|
||||
}
|
||||
173
vendor/github.com/reiver/go-telnet/data_reader.go
generated
vendored
Normal file
173
vendor/github.com/reiver/go-telnet/data_reader.go
generated
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
package telnet
|
||||
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
|
||||
var (
|
||||
errCorrupted = errors.New("Corrupted")
|
||||
)
|
||||
|
||||
|
||||
// An internalDataReader deals with "un-escaping" according to the TELNET protocol.
|
||||
//
|
||||
// In the TELNET protocol byte value 255 is special.
|
||||
//
|
||||
// The TELNET protocol calls byte value 255: "IAC". Which is short for "interpret as command".
|
||||
//
|
||||
// The TELNET protocol also has a distinction between 'data' and 'commands'.
|
||||
//
|
||||
//(DataReader is targetted toward TELNET 'data', not TELNET 'commands'.)
|
||||
//
|
||||
// If a byte with value 255 (=IAC) appears in the data, then it must be escaped.
|
||||
//
|
||||
// Escaping byte value 255 (=IAC) in the data is done by putting 2 of them in a row.
|
||||
//
|
||||
// So, for example:
|
||||
//
|
||||
// []byte{255} -> []byte{255, 255}
|
||||
//
|
||||
// Or, for a more complete example, if we started with the following:
|
||||
//
|
||||
// []byte{1, 55, 2, 155, 3, 255, 4, 40, 255, 30, 20}
|
||||
//
|
||||
// ... TELNET escaping would produce the following:
|
||||
//
|
||||
// []byte{1, 55, 2, 155, 3, 255, 255, 4, 40, 255, 255, 30, 20}
|
||||
//
|
||||
// (Notice that each "255" in the original byte array became 2 "255"s in a row.)
|
||||
//
|
||||
// DataReader deals with "un-escaping". In other words, it un-does what was shown
|
||||
// in the examples.
|
||||
//
|
||||
// So, for example, it does this:
|
||||
//
|
||||
// []byte{255, 255} -> []byte{255}
|
||||
//
|
||||
// And, for example, goes from this:
|
||||
//
|
||||
// []byte{1, 55, 2, 155, 3, 255, 255, 4, 40, 255, 255, 30, 20}
|
||||
//
|
||||
// ... to this:
|
||||
//
|
||||
// []byte{1, 55, 2, 155, 3, 255, 4, 40, 255, 30, 20}
|
||||
type internalDataReader struct {
|
||||
wrapped io.Reader
|
||||
buffered *bufio.Reader
|
||||
}
|
||||
|
||||
|
||||
// newDataReader creates a new DataReader reading from 'r'.
|
||||
func newDataReader(r io.Reader) *internalDataReader {
|
||||
buffered := bufio.NewReader(r)
|
||||
|
||||
reader := internalDataReader{
|
||||
wrapped:r,
|
||||
buffered:buffered,
|
||||
}
|
||||
|
||||
return &reader
|
||||
}
|
||||
|
||||
|
||||
// Read reads the TELNET escaped data from the wrapped io.Reader, and "un-escapes" it into 'data'.
|
||||
func (r *internalDataReader) Read(data []byte) (n int, err error) {
|
||||
|
||||
const IAC = 255
|
||||
|
||||
const SB = 250
|
||||
const SE = 240
|
||||
|
||||
const WILL = 251
|
||||
const WONT = 252
|
||||
const DO = 253
|
||||
const DONT = 254
|
||||
|
||||
p := data
|
||||
|
||||
for len(p) > 0 {
|
||||
var b byte
|
||||
|
||||
b, err = r.buffered.ReadByte()
|
||||
if nil != err {
|
||||
return n, err
|
||||
}
|
||||
|
||||
if IAC == b {
|
||||
var peeked []byte
|
||||
|
||||
peeked, err = r.buffered.Peek(1)
|
||||
if nil != err {
|
||||
return n, err
|
||||
}
|
||||
|
||||
switch peeked[0] {
|
||||
case WILL, WONT, DO, DONT:
|
||||
_, err = r.buffered.Discard(2)
|
||||
if nil != err {
|
||||
return n, err
|
||||
}
|
||||
case IAC:
|
||||
p[0] = IAC
|
||||
n++
|
||||
p = p[1:]
|
||||
|
||||
_, err = r.buffered.Discard(1)
|
||||
if nil != err {
|
||||
return n, err
|
||||
}
|
||||
case SB:
|
||||
for {
|
||||
var b2 byte
|
||||
b2, err = r.buffered.ReadByte()
|
||||
if nil != err {
|
||||
return n, err
|
||||
}
|
||||
|
||||
if IAC == b2 {
|
||||
peeked, err = r.buffered.Peek(1)
|
||||
if nil != err {
|
||||
return n, err
|
||||
}
|
||||
|
||||
if IAC == peeked[0] {
|
||||
_, err = r.buffered.Discard(1)
|
||||
if nil != err {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
if SE == peeked[0] {
|
||||
_, err = r.buffered.Discard(1)
|
||||
if nil != err {
|
||||
return n, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case SE:
|
||||
_, err = r.buffered.Discard(1)
|
||||
if nil != err {
|
||||
return n, err
|
||||
}
|
||||
default:
|
||||
// If we get in here, this is not following the TELNET protocol.
|
||||
//@TODO: Make a better error.
|
||||
err = errCorrupted
|
||||
return n, err
|
||||
}
|
||||
} else {
|
||||
|
||||
p[0] = b
|
||||
n++
|
||||
p = p[1:]
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
142
vendor/github.com/reiver/go-telnet/data_writer.go
generated
vendored
Normal file
142
vendor/github.com/reiver/go-telnet/data_writer.go
generated
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
package telnet
|
||||
|
||||
|
||||
import (
|
||||
"github.com/reiver/go-oi"
|
||||
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
|
||||
var iaciac []byte = []byte{255, 255}
|
||||
|
||||
var errOverflow = errors.New("Overflow")
|
||||
var errPartialIACIACWrite = errors.New("Partial IAC IAC write.")
|
||||
|
||||
|
||||
// An internalDataWriter deals with "escaping" according to the TELNET (and TELNETS) protocol.
|
||||
//
|
||||
// In the TELNET (and TELNETS) protocol byte value 255 is special.
|
||||
//
|
||||
// The TELNET (and TELNETS) protocol calls byte value 255: "IAC". Which is short for "interpret as command".
|
||||
//
|
||||
// The TELNET (and TELNETS) protocol also has a distinction between 'data' and 'commands'.
|
||||
//
|
||||
//(DataWriter is targetted toward TELNET (and TELNETS) 'data', not TELNET (and TELNETS) 'commands'.)
|
||||
//
|
||||
// If a byte with value 255 (=IAC) appears in the data, then it must be escaped.
|
||||
//
|
||||
// Escaping byte value 255 (=IAC) in the data is done by putting 2 of them in a row.
|
||||
//
|
||||
// So, for example:
|
||||
//
|
||||
// []byte{255} -> []byte{255, 255}
|
||||
//
|
||||
// Or, for a more complete example, if we started with the following:
|
||||
//
|
||||
// []byte{1, 55, 2, 155, 3, 255, 4, 40, 255, 30, 20}
|
||||
//
|
||||
// ... TELNET escaping would produce the following:
|
||||
//
|
||||
// []byte{1, 55, 2, 155, 3, 255, 255, 4, 40, 255, 255, 30, 20}
|
||||
//
|
||||
// (Notice that each "255" in the original byte array became 2 "255"s in a row.)
|
||||
//
|
||||
// internalDataWriter takes care of all this for you, so you do not have to do it.
|
||||
type internalDataWriter struct {
|
||||
wrapped io.Writer
|
||||
}
|
||||
|
||||
|
||||
// newDataWriter creates a new internalDataWriter writing to 'w'.
|
||||
//
|
||||
// 'w' receives what is written to the *internalDataWriter but escaped according to
|
||||
// the TELNET (and TELNETS) protocol.
|
||||
//
|
||||
// I.e., byte 255 (= IAC) gets encoded as 255, 255.
|
||||
//
|
||||
// For example, if the following it written to the *internalDataWriter's Write method:
|
||||
//
|
||||
// []byte{1, 55, 2, 155, 3, 255, 4, 40, 255, 30, 20}
|
||||
//
|
||||
// ... then (conceptually) the following is written to 'w's Write method:
|
||||
//
|
||||
// []byte{1, 55, 2, 155, 3, 255, 255, 4, 40, 255, 255, 30, 20}
|
||||
//
|
||||
// (Notice that each "255" in the original byte array became 2 "255"s in a row.)
|
||||
//
|
||||
// *internalDataWriter takes care of all this for you, so you do not have to do it.
|
||||
func newDataWriter(w io.Writer) *internalDataWriter {
|
||||
writer := internalDataWriter{
|
||||
wrapped:w,
|
||||
}
|
||||
|
||||
return &writer
|
||||
}
|
||||
|
||||
|
||||
// Write writes the TELNET (and TELNETS) escaped data for of the data in 'data' to the wrapped io.Writer.
|
||||
func (w *internalDataWriter) Write(data []byte) (n int, err error) {
|
||||
var n64 int64
|
||||
|
||||
n64, err = w.write64(data)
|
||||
n = int(n64)
|
||||
if int64(n) != n64 {
|
||||
panic(errOverflow)
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
|
||||
func (w *internalDataWriter) write64(data []byte) (n int64, err error) {
|
||||
|
||||
if len(data) <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
const IAC = 255
|
||||
|
||||
var buffer bytes.Buffer
|
||||
for _, datum := range data {
|
||||
|
||||
if IAC == datum {
|
||||
|
||||
if buffer.Len() > 0 {
|
||||
var numWritten int64
|
||||
|
||||
numWritten, err = oi.LongWrite(w.wrapped, buffer.Bytes())
|
||||
n += numWritten
|
||||
if nil != err {
|
||||
return n, err
|
||||
}
|
||||
buffer.Reset()
|
||||
}
|
||||
|
||||
|
||||
var numWritten int64
|
||||
//@TODO: Should we worry about "iaciac" potentially being modified by the .Write()?
|
||||
numWritten, err = oi.LongWrite(w.wrapped, iaciac)
|
||||
if int64(len(iaciac)) != numWritten {
|
||||
//@TODO: Do we really want to panic() here?
|
||||
panic(errPartialIACIACWrite)
|
||||
}
|
||||
n += 1
|
||||
if nil != err {
|
||||
return n, err
|
||||
}
|
||||
} else {
|
||||
buffer.WriteByte(datum) // The returned error is always nil, so we ignore it.
|
||||
}
|
||||
}
|
||||
|
||||
if buffer.Len() > 0 {
|
||||
var numWritten int64
|
||||
numWritten, err = oi.LongWrite(w.wrapped, buffer.Bytes())
|
||||
n += numWritten
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
21
vendor/github.com/reiver/go-telnet/discard_logger.go
generated
vendored
Normal file
21
vendor/github.com/reiver/go-telnet/discard_logger.go
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
package telnet
|
||||
|
||||
|
||||
type internalDiscardLogger struct{}
|
||||
|
||||
func (internalDiscardLogger) Debug(...interface{}) {}
|
||||
func (internalDiscardLogger) Debugf(string, ...interface{}) {}
|
||||
func (internalDiscardLogger) Debugln(...interface{}) {}
|
||||
|
||||
func (internalDiscardLogger) Error(...interface{}) {}
|
||||
func (internalDiscardLogger) Errorf(string, ...interface{}) {}
|
||||
func (internalDiscardLogger) Errorln(...interface{}) {}
|
||||
|
||||
func (internalDiscardLogger) Trace(...interface{}) {}
|
||||
func (internalDiscardLogger) Tracef(string, ...interface{}) {}
|
||||
func (internalDiscardLogger) Traceln(...interface{}) {}
|
||||
|
||||
func (internalDiscardLogger) Warn(...interface{}) {}
|
||||
func (internalDiscardLogger) Warnf(string, ...interface{}) {}
|
||||
func (internalDiscardLogger) Warnln(...interface{}) {}
|
||||
|
||||
450
vendor/github.com/reiver/go-telnet/doc.go
generated
vendored
Normal file
450
vendor/github.com/reiver/go-telnet/doc.go
generated
vendored
Normal file
@@ -0,0 +1,450 @@
|
||||
/*
|
||||
Package telnet provides TELNET and TELNETS client and server implementations
|
||||
in a style similar to the "net/http" library that is part of the Go standard library,
|
||||
including support for "middleware"; TELNETS is secure TELNET, with the TELNET protocol
|
||||
over a secured TLS (or SSL) connection.
|
||||
|
||||
|
||||
Example TELNET Server
|
||||
|
||||
ListenAndServe starts a (un-secure) TELNET server with a given address and handler.
|
||||
|
||||
handler := telnet.EchoHandler
|
||||
|
||||
err := telnet.ListenAndServe(":23", handler)
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
Example TELNETS Server
|
||||
|
||||
ListenAndServeTLS starts a (secure) TELNETS server with a given address and handler,
|
||||
using the specified "cert.pem" and "key.pem" files.
|
||||
|
||||
handler := telnet.EchoHandler
|
||||
|
||||
err := telnet.ListenAndServeTLS(":992", "cert.pem", "key.pem", handler)
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
Example TELNET Client:
|
||||
|
||||
DialToAndCall creates a (un-secure) TELNET client, which connects to a given address using the specified caller.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/reiver/go-telnet"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var caller Caller = telnet.StandardCaller
|
||||
|
||||
//@TOOD: replace "example.net:23" with address you want to connect to.
|
||||
telnet.DialToAndCall("example.net:23", caller)
|
||||
}
|
||||
|
||||
|
||||
Example TELNETS Client:
|
||||
|
||||
DialToAndCallTLS creates a (secure) TELNETS client, which connects to a given address using the specified caller.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/reiver/go-telnet"
|
||||
|
||||
"crypto/tls"
|
||||
)
|
||||
|
||||
func main() {
|
||||
//@TODO: Configure the TLS connection here, if you need to.
|
||||
tlsConfig := &tls.Config{}
|
||||
|
||||
var caller Caller = telnet.StandardCaller
|
||||
|
||||
//@TOOD: replace "example.net:992" with address you want to connect to.
|
||||
telnet.DialToAndCallTLS("example.net:992", caller, tlsConfig)
|
||||
}
|
||||
|
||||
|
||||
TELNET vs TELNETS
|
||||
|
||||
If you are communicating over the open Internet, you should be using (the secure) TELNETS protocol and ListenAndServeTLS.
|
||||
|
||||
If you are communicating just on localhost, then using just (the un-secure) TELNET protocol and telnet.ListenAndServe may be OK.
|
||||
|
||||
If you are not sure which to use, use TELNETS and ListenAndServeTLS.
|
||||
|
||||
|
||||
Example TELNET Shell Server
|
||||
|
||||
The previous 2 exaple servers were very very simple. Specifically, they just echoed back whatever
|
||||
you submitted to it.
|
||||
|
||||
If you typed:
|
||||
|
||||
Apple Banana Cherry\r\n
|
||||
|
||||
... it would send back:
|
||||
|
||||
Apple Banana Cherry\r\n
|
||||
|
||||
(Exactly the same data you sent it.)
|
||||
|
||||
A more useful TELNET server can be made using the "github.com/reiver/go-telnet/telsh" sub-package.
|
||||
|
||||
The `telsh` sub-package provides "middleware" that enables you to create a "shell" interface (also
|
||||
called a "command line interface" or "CLI") which most people would expect when using TELNET OR TELNETS.
|
||||
|
||||
For example:
|
||||
|
||||
|
||||
package main
|
||||
|
||||
|
||||
import (
|
||||
"github.com/reiver/go-oi"
|
||||
"github.com/reiver/go-telnet"
|
||||
"github.com/reiver/go-telnet/telsh"
|
||||
|
||||
"time"
|
||||
)
|
||||
|
||||
|
||||
func main() {
|
||||
|
||||
shellHandler := telsh.NewShellHandler()
|
||||
|
||||
commandName := "date"
|
||||
shellHandler.Register(commandName, danceProducer)
|
||||
|
||||
commandName = "animate"
|
||||
shellHandler.Register(commandName, animateProducer)
|
||||
|
||||
addr := ":23"
|
||||
if err := telnet.ListenAndServe(addr, shellHandler); nil != err {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
Note that in the example, so far, we have registered 2 commands: `date` and `animate`.
|
||||
|
||||
For this to actually work, we need to have code for the `date` and `animate` commands.
|
||||
|
||||
The actual implemenation for the `date` command could be done like the following:
|
||||
|
||||
func dateHandlerFunc(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser) error {
|
||||
const layout = "Mon Jan 2 15:04:05 -0700 MST 2006"
|
||||
s := time.Now().Format(layout)
|
||||
|
||||
if _, err := oi.LongWriteString(stdout, s); nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func dateProducerFunc(ctx telsh.Context, name string, args ...string) telsh.Handler{
|
||||
return telsh.PromoteHandlerFunc(dateHandler)
|
||||
}
|
||||
|
||||
|
||||
var dateProducer = ProducerFunc(dateProducerFunc)
|
||||
|
||||
Note that your "real" work is in the `dateHandlerFunc` func.
|
||||
|
||||
And the actual implementation for the `animate` command could be done as follows:
|
||||
|
||||
func animateHandlerFunc(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser) error {
|
||||
|
||||
for i:=0; i<20; i++ {
|
||||
oi.LongWriteString(stdout, "\r⠋")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠙")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠹")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠸")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠼")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠴")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠦")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠧")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠇")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
|
||||
oi.LongWriteString(stdout, "\r⠏")
|
||||
time.Sleep(50*time.Millisecond)
|
||||
}
|
||||
oi.LongWriteString(stdout, "\r \r\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func animateProducerFunc(ctx telsh.Context, name string, args ...string) telsh.Handler{
|
||||
return telsh.PromoteHandlerFunc(animateHandler)
|
||||
}
|
||||
|
||||
|
||||
var animateProducer = ProducerFunc(animateProducerFunc)
|
||||
|
||||
Again, note that your "real" work is in the `animateHandlerFunc` func.
|
||||
|
||||
Generating PEM Files
|
||||
|
||||
If you are using the telnet.ListenAndServeTLS func or the telnet.Server.ListenAndServeTLS method, you will need to
|
||||
supply "cert.pem" and "key.pem" files.
|
||||
|
||||
If you do not already have these files, the Go soure code contains a tool for generating these files for you.
|
||||
|
||||
It can be found at:
|
||||
|
||||
$GOROOT/src/crypto/tls/generate_cert.go
|
||||
|
||||
So, for example, if your `$GOROOT` is the "/usr/local/go" directory, then it would be at:
|
||||
|
||||
/usr/local/go/src/crypto/tls/generate_cert.go
|
||||
|
||||
If you run the command:
|
||||
|
||||
go run $GOROOT/src/crypto/tls/generate_cert.go --help
|
||||
|
||||
... then you get the help information for "generate_cert.go".
|
||||
|
||||
Of course, you would replace or set `$GOROOT` with whatever your path actually is. Again, for example,
|
||||
if your `$GOROOT` is the "/usr/local/go" directory, then it would be:
|
||||
|
||||
go run /usr/local/go/src/crypto/tls/generate_cert.go --help
|
||||
|
||||
To demonstrate the usage of "generate_cert.go", you might run the following to generate certificates
|
||||
that were bound to the hosts `127.0.0.1` and `localhost`:
|
||||
|
||||
go run /usr/local/go/src/crypto/tls/generate_cert.go --ca --host='127.0.0.1,localhost'
|
||||
|
||||
|
||||
If you are not sure where "generate_cert.go" is on your computer, on Linux and Unix based systems, you might
|
||||
be able to find the file with the command:
|
||||
|
||||
locate /src/crypto/tls/generate_cert.go
|
||||
|
||||
(If it finds it, it should output the full path to this file.)
|
||||
|
||||
|
||||
Example TELNET Client
|
||||
|
||||
You can make a simple (un-secure) TELNET client with code like the following:
|
||||
|
||||
package main
|
||||
|
||||
|
||||
import (
|
||||
"github.com/reiver/go-telnet"
|
||||
)
|
||||
|
||||
|
||||
func main() {
|
||||
var caller Caller = telnet.StandardCaller
|
||||
|
||||
//@TOOD: replace "example.net:5555" with address you want to connect to.
|
||||
telnet.DialToAndCall("example.net:5555", caller)
|
||||
}
|
||||
|
||||
|
||||
Example TELNETS Client
|
||||
|
||||
You can make a simple (secure) TELNETS client with code like the following:
|
||||
|
||||
package main
|
||||
|
||||
|
||||
import (
|
||||
"github.com/reiver/go-telnet"
|
||||
)
|
||||
|
||||
|
||||
func main() {
|
||||
var caller Caller = telnet.StandardCaller
|
||||
|
||||
//@TOOD: replace "example.net:5555" with address you want to connect to.
|
||||
telnet.DialToAndCallTLS("example.net:5555", caller)
|
||||
}
|
||||
|
||||
|
||||
TELNET Story
|
||||
|
||||
The TELNET protocol is best known for providing a means of connecting to a remote computer, using a (text-based) shell interface, and being able to interact with it, (more or less) as if you were sitting at that computer.
|
||||
|
||||
(Shells are also known as command-line interfaces or CLIs.)
|
||||
|
||||
Although this was the original usage of the TELNET protocol, it can be (and is) used for other purposes as well.
|
||||
|
||||
|
||||
The Era
|
||||
|
||||
The TELNET protocol came from an era in computing when text-based shell interface where the common way of interacting with computers.
|
||||
|
||||
The common interface for computers during this era was a keyboard and a monochromatic (i.e., single color) text-based monitors called "video terminals".
|
||||
|
||||
(The word "video" in that era of computing did not refer to things such as movies. But instead was meant to contrast it with paper. In particular, the teletype machines, which were typewriter like devices that had a keyboard, but instead of having a monitor had paper that was printed onto.)
|
||||
|
||||
|
||||
Early Office Computers
|
||||
|
||||
In that era, in the early days of office computers, it was rare that an individual would have a computer at their desk. (A single computer was much too expensive.)
|
||||
|
||||
Instead, there would be a single central computer that everyone would share. The style of computer used (for the single central shared computer) was called a "mainframe".
|
||||
|
||||
What individuals would have at their desks, instead of their own compuer, would be some type of video terminal.
|
||||
|
||||
The different types of video terminals had named such as:
|
||||
|
||||
• VT52
|
||||
|
||||
• VT100
|
||||
|
||||
• VT220
|
||||
|
||||
• VT240
|
||||
|
||||
("VT" in those named was short for "video terminal".)
|
||||
|
||||
|
||||
Teletype
|
||||
|
||||
To understand this era, we need to go back a bit in time to what came before it: teletypes.
|
||||
|
||||
|
||||
Terminal Codes
|
||||
|
||||
Terminal codes (also sometimes called 'terminal control codes') are used to issue various kinds of commands
|
||||
to the terminal.
|
||||
|
||||
(Note that 'terminal control codes' are a completely separate concept for 'TELNET commands',
|
||||
and the two should NOT be conflated or confused.)
|
||||
|
||||
The most common types of 'terminal codes' are the 'ANSI escape codes'. (Although there are other types too.)
|
||||
|
||||
|
||||
ANSI Escape Codes
|
||||
|
||||
ANSI escape codes (also sometimes called 'ANSI escape sequences') are a common type of 'terminal code' used
|
||||
to do things such as:
|
||||
|
||||
• moving the cursor,
|
||||
|
||||
• erasing the display,
|
||||
|
||||
• erasing the line,
|
||||
|
||||
• setting the graphics mode,
|
||||
|
||||
• setting the foregroup color,
|
||||
|
||||
• setting the background color,
|
||||
|
||||
• setting the screen resolution, and
|
||||
|
||||
• setting keyboard strings.
|
||||
|
||||
|
||||
Setting The Foreground Color With ANSI Escape Codes
|
||||
|
||||
One of the abilities of ANSI escape codes is to set the foreground color.
|
||||
|
||||
Here is a table showing codes for this:
|
||||
|
||||
| ANSI Color | Go string | Go []byte |
|
||||
| ------------ | ---------- | ----------------------------- |
|
||||
| Black | "\x1b[30m" | []byte{27, '[', '3','0', 'm'} |
|
||||
| Red | "\x1b[31m" | []byte{27, '[', '3','1', 'm'} |
|
||||
| Green | "\x1b[32m" | []byte{27, '[', '3','2', 'm'} |
|
||||
| Brown/Yellow | "\x1b[33m" | []byte{27, '[', '3','3', 'm'} |
|
||||
| Blue | "\x1b[34m" | []byte{27, '[', '3','4', 'm'} |
|
||||
| Magenta | "\x1b[35m" | []byte{27, '[', '3','5', 'm'} |
|
||||
| Cyan | "\x1b[36m" | []byte{27, '[', '3','6', 'm'} |
|
||||
| Gray/White | "\x1b[37m" | []byte{27, '[', '3','7', 'm'} |
|
||||
|
||||
(Note that in the `[]byte` that the first `byte` is the number `27` (which
|
||||
is the "escape" character) where the third and fouth characters are the
|
||||
**not** number literals, but instead character literals `'3'` and whatever.)
|
||||
|
||||
|
||||
Setting The Background Color With ANSI Escape Codes
|
||||
|
||||
Another of the abilities of ANSI escape codes is to set the background color.
|
||||
|
||||
| ANSI Color | Go string | Go []byte |
|
||||
| ------------ | ---------- | ----------------------------- |
|
||||
| Black | "\x1b[40m" | []byte{27, '[', '4','0', 'm'} |
|
||||
| Red | "\x1b[41m" | []byte{27, '[', '4','1', 'm'} |
|
||||
| Green | "\x1b[42m" | []byte{27, '[', '4','2', 'm'} |
|
||||
| Brown/Yellow | "\x1b[43m" | []byte{27, '[', '4','3', 'm'} |
|
||||
| Blue | "\x1b[44m" | []byte{27, '[', '4','4', 'm'} |
|
||||
| Magenta | "\x1b[45m" | []byte{27, '[', '4','5', 'm'} |
|
||||
| Cyan | "\x1b[46m" | []byte{27, '[', '4','6', 'm'} |
|
||||
| Gray/White | "\x1b[47m" | []byte{27, '[', '4','7', 'm'} |
|
||||
|
||||
(Note that in the `[]byte` that the first `byte` is the number `27` (which
|
||||
is the "escape" character) where the third and fouth characters are the
|
||||
**not** number literals, but instead character literals `'4'` and whatever.)
|
||||
|
||||
Using ANSI Escape Codes
|
||||
|
||||
In Go code, if I wanted to use an ANSI escape code to use a blue background,
|
||||
a white foreground, and bold, I could do that with the ANSI escape code:
|
||||
|
||||
"\x1b[44;37;1m"
|
||||
|
||||
Note that that start with byte value 27, which we have encoded as hexadecimal
|
||||
as \x1b. Followed by the '[' character.
|
||||
|
||||
Coming after that is the sub-string "44", which is the code that sets our background color to blue.
|
||||
|
||||
We follow that with the ';' character (which separates codes).
|
||||
|
||||
And the after that comes the sub-string "37", which is the code that set our foreground color to white.
|
||||
|
||||
After that, we follow with another ";" character (which, again, separates codes).
|
||||
|
||||
And then we follow it the sub-string "1", which is the code that makes things bold.
|
||||
|
||||
And finally, the ANSI escape sequence is finished off with the 'm' character.
|
||||
|
||||
To show this in a more complete example, our `dateHandlerFunc` from before could incorporate ANSI escape sequences as follows:
|
||||
|
||||
func dateHandlerFunc(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser) error {
|
||||
const layout = "Mon Jan 2 15:04:05 -0700 MST 2006"
|
||||
s := "\x1b[44;37;1m" + time.Now().Format(layout) + "\x1b[0m"
|
||||
|
||||
if _, err := oi.LongWriteString(stdout, s); nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Note that in that example, in addition to using the ANSI escape sequence "\x1b[44;37;1m"
|
||||
to set the background color to blue, set the foreground color to white, and make it bold,
|
||||
we also used the ANSI escape sequence "\x1b[0m" to reset the background and foreground colors
|
||||
and boldness back to "normal".
|
||||
|
||||
*/
|
||||
package telnet
|
||||
33
vendor/github.com/reiver/go-telnet/echo_handler.go
generated
vendored
Normal file
33
vendor/github.com/reiver/go-telnet/echo_handler.go
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
package telnet
|
||||
|
||||
|
||||
import (
|
||||
"github.com/reiver/go-oi"
|
||||
)
|
||||
|
||||
|
||||
// EchoHandler is a simple TELNET server which "echos" back to the client any (non-command)
|
||||
// data back to the TELNET client, it received from the TELNET client.
|
||||
var EchoHandler Handler = internalEchoHandler{}
|
||||
|
||||
|
||||
type internalEchoHandler struct{}
|
||||
|
||||
|
||||
func (handler internalEchoHandler) ServeTELNET(ctx Context, w Writer, r 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 {
|
||||
n, err := r.Read(p)
|
||||
|
||||
if n > 0 {
|
||||
oi.LongWrite(w, p[:n])
|
||||
}
|
||||
|
||||
if nil != err {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
18
vendor/github.com/reiver/go-telnet/handler.go
generated
vendored
Normal file
18
vendor/github.com/reiver/go-telnet/handler.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package telnet
|
||||
|
||||
|
||||
// A Handler serves a TELNET (or TELNETS) connection.
|
||||
//
|
||||
// Writing data to the Writer passed as an argument to the ServeTELNET method
|
||||
// will send data to the TELNET (or TELNETS) client.
|
||||
//
|
||||
// Reading data from the Reader passed as an argument to the ServeTELNET method
|
||||
// will receive data from the TELNET client.
|
||||
//
|
||||
// The Writer's Write method sends "escaped" TELNET (and TELNETS) data.
|
||||
//
|
||||
// The Reader's Read method "un-escapes" TELNET (and TELNETS) data, and filters
|
||||
// out TELNET (and TELNETS) command sequences.
|
||||
type Handler interface {
|
||||
ServeTELNET(Context, Writer, Reader)
|
||||
}
|
||||
16
vendor/github.com/reiver/go-telnet/logger.go
generated
vendored
Normal file
16
vendor/github.com/reiver/go-telnet/logger.go
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package telnet
|
||||
|
||||
|
||||
type Logger interface{
|
||||
Debug(...interface{})
|
||||
Debugf(string, ...interface{})
|
||||
|
||||
Error(...interface{})
|
||||
Errorf(string, ...interface{})
|
||||
|
||||
Trace(...interface{})
|
||||
Tracef(string, ...interface{})
|
||||
|
||||
Warn(...interface{})
|
||||
Warnf(string, ...interface{})
|
||||
}
|
||||
6
vendor/github.com/reiver/go-telnet/reader.go
generated
vendored
Normal file
6
vendor/github.com/reiver/go-telnet/reader.go
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
package telnet
|
||||
|
||||
|
||||
type Reader interface {
|
||||
Read([]byte) (int, error)
|
||||
}
|
||||
191
vendor/github.com/reiver/go-telnet/server.go
generated
vendored
Normal file
191
vendor/github.com/reiver/go-telnet/server.go
generated
vendored
Normal file
@@ -0,0 +1,191 @@
|
||||
package telnet
|
||||
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
)
|
||||
|
||||
|
||||
// ListenAndServe listens on the TCP network address `addr` and then spawns a call to the ServeTELNET
|
||||
// method on the `handler` to serve each incoming connection.
|
||||
//
|
||||
// For a very simple example:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "github.com/reiver/go-telnet"
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
//
|
||||
// //@TODO: In your code, you would probably want to use a different handler.
|
||||
// var handler telnet.Handler = telnet.EchoHandler
|
||||
//
|
||||
// err := telnet.ListenAndServe(":5555", handler)
|
||||
// if nil != err {
|
||||
// //@TODO: Handle this error better.
|
||||
// panic(err)
|
||||
// }
|
||||
// }
|
||||
func ListenAndServe(addr string, handler Handler) error {
|
||||
server := &Server{Addr: addr, Handler: handler}
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
|
||||
|
||||
// Serve accepts an incoming TELNET or TELNETS client connection on the net.Listener `listener`.
|
||||
func Serve(listener net.Listener, handler Handler) error {
|
||||
|
||||
server := &Server{Handler: handler}
|
||||
return server.Serve(listener)
|
||||
}
|
||||
|
||||
|
||||
// A Server defines parameters of a running TELNET server.
|
||||
//
|
||||
// For a simple example:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "github.com/reiver/go-telnet"
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
//
|
||||
// var handler telnet.Handler = telnet.EchoHandler
|
||||
//
|
||||
// server := &telnet.Server{
|
||||
// Addr:":5555",
|
||||
// Handler:handler,
|
||||
// }
|
||||
//
|
||||
// err := server.ListenAndServe()
|
||||
// if nil != err {
|
||||
// //@TODO: Handle this error better.
|
||||
// panic(err)
|
||||
// }
|
||||
// }
|
||||
type Server struct {
|
||||
Addr string // TCP address to listen on; ":telnet" or ":telnets" if empty (when used with ListenAndServe or ListenAndServeTLS respectively).
|
||||
Handler Handler // handler to invoke; telnet.EchoServer if nil
|
||||
|
||||
TLSConfig *tls.Config // optional TLS configuration; used by ListenAndServeTLS.
|
||||
|
||||
Logger Logger
|
||||
}
|
||||
|
||||
// ListenAndServe listens on the TCP network address 'server.Addr' and then spawns a call to the ServeTELNET
|
||||
// method on the 'server.Handler' to serve each incoming connection.
|
||||
//
|
||||
// For a simple example:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "github.com/reiver/go-telnet"
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
//
|
||||
// var handler telnet.Handler = telnet.EchoHandler
|
||||
//
|
||||
// server := &telnet.Server{
|
||||
// Addr:":5555",
|
||||
// Handler:handler,
|
||||
// }
|
||||
//
|
||||
// err := server.ListenAndServe()
|
||||
// if nil != err {
|
||||
// //@TODO: Handle this error better.
|
||||
// panic(err)
|
||||
// }
|
||||
// }
|
||||
func (server *Server) ListenAndServe() error {
|
||||
|
||||
addr := server.Addr
|
||||
if "" == addr {
|
||||
addr = ":telnet"
|
||||
}
|
||||
|
||||
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
return server.Serve(listener)
|
||||
}
|
||||
|
||||
|
||||
// Serve accepts an incoming TELNET client connection on the net.Listener `listener`.
|
||||
func (server *Server) Serve(listener net.Listener) error {
|
||||
|
||||
defer listener.Close()
|
||||
|
||||
|
||||
logger := server.logger()
|
||||
|
||||
|
||||
handler := server.Handler
|
||||
if nil == handler {
|
||||
//@TODO: Should this be a "ShellHandler" instead, that gives a shell-like experience by default
|
||||
// If this is changd, then need to change the comment in the "type Server struct" definition.
|
||||
logger.Debug("Defaulted handler to EchoHandler.")
|
||||
handler = EchoHandler
|
||||
}
|
||||
|
||||
|
||||
for {
|
||||
// Wait for a new TELNET client connection.
|
||||
logger.Debugf("Listening at %q.", listener.Addr())
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
//@TODO: Could try to recover from certain kinds of errors. Maybe waiting a while before trying again.
|
||||
return err
|
||||
}
|
||||
logger.Debugf("Received new connection from %q.", conn.RemoteAddr())
|
||||
|
||||
// Handle the new TELNET client connection by spawning
|
||||
// a new goroutine.
|
||||
go server.handle(conn, handler)
|
||||
logger.Debugf("Spawned handler to handle connection from %q.", conn.RemoteAddr())
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) handle(c net.Conn, handler Handler) {
|
||||
defer c.Close()
|
||||
|
||||
logger := server.logger()
|
||||
|
||||
|
||||
defer func(){
|
||||
if r := recover(); nil != r {
|
||||
if nil != logger {
|
||||
logger.Errorf("Recovered from: (%T) %v", r, r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var ctx Context = NewContext().InjectLogger(logger)
|
||||
|
||||
var w Writer = newDataWriter(c)
|
||||
var r Reader = newDataReader(c)
|
||||
|
||||
handler.ServeTELNET(ctx, w, r)
|
||||
c.Close()
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (server *Server) logger() Logger {
|
||||
logger := server.Logger
|
||||
if nil == logger {
|
||||
logger = internalDiscardLogger{}
|
||||
}
|
||||
|
||||
return logger
|
||||
}
|
||||
93
vendor/github.com/reiver/go-telnet/standard_caller.go
generated
vendored
Normal file
93
vendor/github.com/reiver/go-telnet/standard_caller.go
generated
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
package telnet
|
||||
|
||||
|
||||
import (
|
||||
"github.com/reiver/go-oi"
|
||||
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"time"
|
||||
)
|
||||
|
||||
|
||||
// StandardCaller is a simple TELNET client which sends to the server any data it gets from os.Stdin
|
||||
// as TELNET (and TELNETS) data, and writes any TELNET (or TELNETS) data it receives from
|
||||
// the server to os.Stdout, and writes any error it has to os.Stderr.
|
||||
var StandardCaller Caller = internalStandardCaller{}
|
||||
|
||||
|
||||
type internalStandardCaller struct{}
|
||||
|
||||
|
||||
func (caller internalStandardCaller) CallTELNET(ctx Context, w Writer, r Reader) {
|
||||
standardCallerCallTELNET(os.Stdin, os.Stdout, os.Stderr, ctx, w, r)
|
||||
}
|
||||
|
||||
|
||||
func standardCallerCallTELNET(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser, ctx Context, w Writer, r 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 && nil == err {
|
||||
continue
|
||||
} else if n <= 0 && nil != err {
|
||||
break
|
||||
}
|
||||
|
||||
oi.LongWrite(writer, p)
|
||||
}
|
||||
}(stdout, r)
|
||||
|
||||
|
||||
|
||||
var buffer bytes.Buffer
|
||||
var p []byte
|
||||
|
||||
var crlfBuffer [2]byte = [2]byte{'\r','\n'}
|
||||
crlf := crlfBuffer[:]
|
||||
|
||||
scanner := bufio.NewScanner(stdin)
|
||||
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(stderr, 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)
|
||||
}
|
||||
113
vendor/github.com/reiver/go-telnet/tls.go
generated
vendored
Normal file
113
vendor/github.com/reiver/go-telnet/tls.go
generated
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
package telnet
|
||||
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
)
|
||||
|
||||
|
||||
// ListenAndServeTLS acts identically to ListenAndServe, except that it
|
||||
// uses the TELNET protocol over TLS.
|
||||
//
|
||||
// From a TELNET protocol point-of-view, it allows for 'secured telnet', also known as TELNETS,
|
||||
// which by default listens to port 992.
|
||||
//
|
||||
// Of course, this port can be overridden using the 'addr' argument.
|
||||
//
|
||||
// For a very simple example:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "github.com/reiver/go-telnet"
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
//
|
||||
// //@TODO: In your code, you would probably want to use a different handler.
|
||||
// var handler telnet.Handler = telnet.EchoHandler
|
||||
//
|
||||
// err := telnet.ListenAndServeTLS(":5555", "cert.pem", "key.pem", handler)
|
||||
// if nil != err {
|
||||
// //@TODO: Handle this error better.
|
||||
// panic(err)
|
||||
// }
|
||||
// }
|
||||
func ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler) error {
|
||||
server := &Server{Addr: addr, Handler: handler}
|
||||
return server.ListenAndServeTLS(certFile, keyFile)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ListenAndServeTLS acts identically to ListenAndServe, except that it
|
||||
// uses the TELNET protocol over TLS.
|
||||
//
|
||||
// From a TELNET protocol point-of-view, it allows for 'secured telnet', also known as TELNETS,
|
||||
// which by default listens to port 992.
|
||||
func (server *Server) ListenAndServeTLS(certFile string, keyFile string) error {
|
||||
|
||||
addr := server.Addr
|
||||
if "" == addr {
|
||||
addr = ":telnets"
|
||||
}
|
||||
|
||||
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
// Apparently have to make a copy of the TLS config this way, rather than by
|
||||
// simple assignment, to prevent some unexported fields from being copied over.
|
||||
//
|
||||
// It would be nice if tls.Config had a method that would do this "safely".
|
||||
// (I.e., what happens if in the future more exported fields are added to
|
||||
// tls.Config?)
|
||||
var tlsConfig *tls.Config = nil
|
||||
if nil == server.TLSConfig {
|
||||
tlsConfig = &tls.Config{}
|
||||
} else {
|
||||
tlsConfig = &tls.Config{
|
||||
Rand: server.TLSConfig.Rand,
|
||||
Time: server.TLSConfig.Time,
|
||||
Certificates: server.TLSConfig.Certificates,
|
||||
NameToCertificate: server.TLSConfig.NameToCertificate,
|
||||
GetCertificate: server.TLSConfig.GetCertificate,
|
||||
RootCAs: server.TLSConfig.RootCAs,
|
||||
NextProtos: server.TLSConfig.NextProtos,
|
||||
ServerName: server.TLSConfig.ServerName,
|
||||
ClientAuth: server.TLSConfig.ClientAuth,
|
||||
ClientCAs: server.TLSConfig.ClientCAs,
|
||||
InsecureSkipVerify: server.TLSConfig.InsecureSkipVerify,
|
||||
CipherSuites: server.TLSConfig.CipherSuites,
|
||||
PreferServerCipherSuites: server.TLSConfig.PreferServerCipherSuites,
|
||||
SessionTicketsDisabled: server.TLSConfig.SessionTicketsDisabled,
|
||||
SessionTicketKey: server.TLSConfig.SessionTicketKey,
|
||||
ClientSessionCache: server.TLSConfig.ClientSessionCache,
|
||||
MinVersion: server.TLSConfig.MinVersion,
|
||||
MaxVersion: server.TLSConfig.MaxVersion,
|
||||
CurvePreferences: server.TLSConfig.CurvePreferences,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tlsConfigHasCertificate := len(tlsConfig.Certificates) > 0 || nil != tlsConfig.GetCertificate
|
||||
if "" == certFile || "" == keyFile || !tlsConfigHasCertificate {
|
||||
tlsConfig.Certificates = make([]tls.Certificate, 1)
|
||||
|
||||
var err error
|
||||
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tlsListener := tls.NewListener(listener, tlsConfig)
|
||||
|
||||
|
||||
return server.Serve(tlsListener)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user