Compare commits
70 Commits
v1.7.0
...
guardrails
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26a7186f40 | ||
|
|
41eeb364f8 | ||
|
|
a22f8f0b7b | ||
|
|
bd1c3609a7 | ||
|
|
c5e75df64f | ||
|
|
6b181dd291 | ||
|
|
4ab88cad10 | ||
|
|
b902953df4 | ||
|
|
e141368734 | ||
|
|
980da40988 | ||
|
|
22d25f1e70 | ||
|
|
84d77d0a9f | ||
|
|
b0afdf933a | ||
|
|
e9eef9a49e | ||
|
|
6f2b58cbdc | ||
|
|
09ac2c35f3 | ||
|
|
47a6fc9906 | ||
|
|
c3d49fde95 | ||
|
|
ec1e4d5c8a | ||
|
|
e65ef7ccc1 | ||
|
|
68e7fd2090 | ||
|
|
b958f8461f | ||
|
|
a08d84e7ed | ||
|
|
2b66d8d56a | ||
|
|
a40789e1f2 | ||
|
|
63571af252 | ||
|
|
75c6840ecd | ||
|
|
e6a02a85f0 | ||
|
|
2c3de75f3d | ||
|
|
7c4aab34ed | ||
|
|
a8480f82e0 | ||
|
|
a5dacca9a1 | ||
|
|
31ba233b34 | ||
|
|
5720123576 | ||
|
|
9cc09b320d | ||
|
|
cb3c1056e5 | ||
|
|
82f96e457c | ||
|
|
062e2b4b8f | ||
|
|
9de51acbcc | ||
|
|
6d3a97cdbc | ||
|
|
3ebcdd9c3d | ||
|
|
a9f86d1d01 | ||
|
|
2a68fc3114 | ||
|
|
2352a53e6e | ||
|
|
fcc94c58d9 | ||
|
|
da9c4920ab | ||
|
|
0295eedb6e | ||
|
|
7f26cc1dbb | ||
|
|
9e1c395810 | ||
|
|
9db4b92d4e | ||
|
|
ff46ee89d9 | ||
|
|
b9af077ef4 | ||
|
|
b23ee4144d | ||
|
|
57f894bfca | ||
|
|
58e2abca8c | ||
|
|
ed676b0d7e | ||
|
|
ed42f343d2 | ||
|
|
2555c478b4 | ||
|
|
6152e55e7d | ||
|
|
023cdd1bb3 | ||
|
|
5efe250466 | ||
|
|
695ddc91dd | ||
|
|
7b30017a14 | ||
|
|
e5542ae266 | ||
|
|
d19b8a53f2 | ||
|
|
2e39f70cd5 | ||
|
|
26c0bb8b1a | ||
|
|
12b0db07da | ||
|
|
7aace9109a | ||
|
|
6c4caea26f |
@@ -28,8 +28,8 @@
|
||||
<!-- known_user_key -->
|
||||
<g id="node2" class="node">
|
||||
<title>known_user_key</title>
|
||||
<polygon fill="none" stroke="#ffa500" 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="#ffa500">known user key</text>
|
||||
<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">
|
||||
@@ -40,8 +40,8 @@
|
||||
<!-- unknown_user_key -->
|
||||
<g id="node3" class="node">
|
||||
<title>unknown_user_key</title>
|
||||
<polygon fill="none" stroke="#ffa500" 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="#ffa500">unknown user key</text>
|
||||
<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">
|
||||
@@ -52,15 +52,15 @@
|
||||
<!-- acl_manager -->
|
||||
<g id="node5" class="node">
|
||||
<title>acl_manager</title>
|
||||
<polygon fill="none" stroke="#ffa500" 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="#ffa500">ACL manager</text>
|
||||
<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="#ffa500" d="M267.461,-177.4127C331.1153,-180.3462 438.21,-185.2816 504.3082,-188.3277"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" 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="#ffa500">user matches an existing host</text>
|
||||
<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">
|
||||
@@ -91,15 +91,15 @@
|
||||
<!-- invite_manager -->
|
||||
<g id="node4" class="node">
|
||||
<title>invite_manager</title>
|
||||
<polygon fill="none" stroke="#ffa500" 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="#ffa500">invite manager</text>
|
||||
<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="#ffa500" d="M274.7912,-80.5452C338.467,-70.4579 438.7527,-54.5711 502.4793,-44.4759"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" 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="#ffa500">user=invite:<token></text>
|
||||
<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">
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -2,15 +2,35 @@ graph {
|
||||
rankdir=LR;
|
||||
node[shape=box,style=rounded,style=rounded,fillcolor=gray];
|
||||
|
||||
db[color=gray];
|
||||
|
||||
user1 -- sshportal -- host1[color=red,penwidth=3.0];
|
||||
user2 -- sshportal -- host2[color=blue,penwidth=3.0];
|
||||
user3 -- sshportal -- host1[color=purple,penwidth=3.0];
|
||||
user2 -- sshportal -- host3[color=green,penwidth=3.0];
|
||||
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];
|
||||
|
||||
{ rank=same; sshportal; db; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,125 +4,146 @@
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: %3 Pages: 1 -->
|
||||
<svg width="255pt" height="206pt"
|
||||
viewBox="0.00 0.00 254.55 206.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 202)">
|
||||
<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,-202 250.5518,-202 250.5518,4 -4,4"/>
|
||||
<!-- db -->
|
||||
<g id="node1" class="node">
|
||||
<title>db</title>
|
||||
<path fill="none" stroke="#c0c0c0" d="M138.2759,-90C138.2759,-90 108.2759,-90 108.2759,-90 102.2759,-90 96.2759,-84 96.2759,-78 96.2759,-78 96.2759,-66 96.2759,-66 96.2759,-60 102.2759,-54 108.2759,-54 108.2759,-54 138.2759,-54 138.2759,-54 144.2759,-54 150.2759,-60 150.2759,-66 150.2759,-66 150.2759,-78 150.2759,-78 150.2759,-84 144.2759,-90 138.2759,-90"/>
|
||||
<text text-anchor="middle" x="123.2759" y="-67.8" font-family="Times,serif" font-size="14.00" fill="#000000">db</text>
|
||||
</g>
|
||||
<!-- user1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>user1</title>
|
||||
<path fill="none" stroke="#000000" 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>
|
||||
<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="node3" class="node">
|
||||
<g id="node1" class="node">
|
||||
<title>sshportal</title>
|
||||
<path fill="none" stroke="#000000" d="M144.3291,-144C144.3291,-144 102.2226,-144 102.2226,-144 96.2226,-144 90.2226,-138 90.2226,-132 90.2226,-132 90.2226,-120 90.2226,-120 90.2226,-114 96.2226,-108 102.2226,-108 102.2226,-108 144.3291,-108 144.3291,-108 150.3291,-108 156.3291,-114 156.3291,-120 156.3291,-120 156.3291,-132 156.3291,-132 156.3291,-138 150.3291,-144 144.3291,-144"/>
|
||||
<text text-anchor="middle" x="123.2759" y="-121.8" font-family="Times,serif" font-size="14.00" fill="#000000">sshportal</text>
|
||||
<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>
|
||||
<!-- user1--sshportal -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>user1--sshportal</title>
|
||||
<path fill="none" stroke="#ff0000" stroke-width="3" d="M54.0744,-164.8143C65.5316,-158.3881 78.9716,-150.8497 90.9443,-144.1344"/>
|
||||
<!-- 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="edge11" class="edge">
|
||||
<g id="edge13" class="edge">
|
||||
<title>sshportal--db</title>
|
||||
<path fill="none" stroke="#c0c0c0" stroke-dasharray="1,5" d="M123.2759,-107.7902C123.2759,-101.907 123.2759,-96.0238 123.2759,-90.1406"/>
|
||||
<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="#000000" d="M234.5518,-198C234.5518,-198 204.5518,-198 204.5518,-198 198.5518,-198 192.5518,-192 192.5518,-186 192.5518,-186 192.5518,-174 192.5518,-174 192.5518,-168 198.5518,-162 204.5518,-162 204.5518,-162 234.5518,-162 234.5518,-162 240.5518,-162 246.5518,-168 246.5518,-174 246.5518,-174 246.5518,-186 246.5518,-186 246.5518,-192 240.5518,-198 234.5518,-198"/>
|
||||
<text text-anchor="middle" x="219.5518" y="-175.8" font-family="Times,serif" font-size="14.00" fill="#000000">host1</text>
|
||||
<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="M156.4086,-138.1341C170.0068,-145.1999 185.38,-154.0479 197.5528,-161.8875"/>
|
||||
<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="M145.1224,-144.0143C159.1032,-153.0284 177.352,-163.3941 192.2807,-170.8554"/>
|
||||
<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="node6" class="node">
|
||||
<g id="node5" class="node">
|
||||
<title>host2</title>
|
||||
<path fill="none" stroke="#000000" d="M234.5518,-144C234.5518,-144 204.5518,-144 204.5518,-144 198.5518,-144 192.5518,-138 192.5518,-132 192.5518,-132 192.5518,-120 192.5518,-120 192.5518,-114 198.5518,-108 204.5518,-108 204.5518,-108 234.5518,-108 234.5518,-108 240.5518,-108 246.5518,-114 246.5518,-120 246.5518,-120 246.5518,-132 246.5518,-132 246.5518,-138 240.5518,-144 234.5518,-144"/>
|
||||
<text text-anchor="middle" x="219.5518" y="-121.8" font-family="Times,serif" font-size="14.00" fill="#000000">host2</text>
|
||||
<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="M156.4086,-126C168.1574,-126 181.2313,-126 192.4212,-126"/>
|
||||
<path fill="none" stroke="#0000ff" stroke-width="3" d="M178.2919,-126C190.3932,-126 203.1534,-126 213.9962,-126"/>
|
||||
</g>
|
||||
<!-- host3 -->
|
||||
<g id="node8" class="node">
|
||||
<g id="node6" class="node">
|
||||
<title>host3</title>
|
||||
<path fill="none" stroke="#000000" d="M234.5518,-90C234.5518,-90 204.5518,-90 204.5518,-90 198.5518,-90 192.5518,-84 192.5518,-78 192.5518,-78 192.5518,-66 192.5518,-66 192.5518,-60 198.5518,-54 204.5518,-54 204.5518,-54 234.5518,-54 234.5518,-54 240.5518,-54 246.5518,-60 246.5518,-66 246.5518,-66 246.5518,-78 246.5518,-78 246.5518,-84 240.5518,-90 234.5518,-90"/>
|
||||
<text text-anchor="middle" x="219.5518" y="-67.8" font-family="Times,serif" font-size="14.00" fill="#000000">host3</text>
|
||||
<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="M155.6022,-107.8686C167.5109,-101.1891 180.872,-93.695 192.2898,-87.2909"/>
|
||||
<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="node10" class="node">
|
||||
<g id="node7" class="node">
|
||||
<title>hostN</title>
|
||||
<path fill="none" stroke="#000000" d="M234.5518,-36C234.5518,-36 204.5518,-36 204.5518,-36 198.5518,-36 192.5518,-30 192.5518,-24 192.5518,-24 192.5518,-12 192.5518,-12 192.5518,-6 198.5518,0 204.5518,0 204.5518,0 234.5518,0 234.5518,0 240.5518,0 246.5518,-6 246.5518,-12 246.5518,-12 246.5518,-24 246.5518,-24 246.5518,-30 240.5518,-36 234.5518,-36"/>
|
||||
<text text-anchor="middle" x="219.5518" y="-13.8" font-family="Times,serif" font-size="14.00" fill="#000000">hostN</text>
|
||||
<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="edge10" class="edge">
|
||||
<g id="edge12" class="edge">
|
||||
<title>sshportal--hostN</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M147.3357,-107.9514C150.577,-105.1054 153.747,-102.0806 156.5518,-99 175.9708,-77.6716 174.2647,-67.3066 192.5518,-45 194.9894,-42.0266 197.7084,-39.0146 200.4561,-36.1341"/>
|
||||
<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="node5" class="node">
|
||||
<g id="node9" class="node">
|
||||
<title>user2</title>
|
||||
<path fill="none" stroke="#000000" 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"/>
|
||||
<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.0744,-119.8607C65.1978,-119.1729 78.1904,-119.0661 89.8945,-119.5402"/>
|
||||
<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.0744,-132.1393C65.1978,-132.8271 78.1904,-132.9339 89.8945,-132.4598"/>
|
||||
<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="node7" class="node">
|
||||
<g id="node10" class="node">
|
||||
<title>user3</title>
|
||||
<path fill="none" stroke="#000000" 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"/>
|
||||
<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.0744,-87.1857C65.5316,-93.6119 78.9716,-101.1503 90.9443,-107.8656"/>
|
||||
<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="node9" class="node">
|
||||
<g id="node11" class="node">
|
||||
<title>userN</title>
|
||||
<path fill="none" stroke="#000000" 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"/>
|
||||
<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="edge9" class="edge">
|
||||
<g id="edge11" class="edge">
|
||||
<title>userN--sshportal</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M46.0956,-36.1341C48.8434,-39.0146 51.5624,-42.0266 54,-45 72.287,-67.3066 70.581,-77.6716 90,-99 92.8048,-102.0806 95.9748,-105.1054 99.216,-107.9514"/>
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 9.2 KiB |
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
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## master (unreleased)
|
||||
|
||||
* No entry
|
||||
|
||||
## v1.8.0 (2018-04-02)
|
||||
|
||||
* The default created user now has the same username as the user starting sshportal (was hardcoded "admin")
|
||||
* Add Telnet support
|
||||
* Add TTY audit feature ([#23](https://github.com/moul/sshportal/issues/23)) by [@sabban](https://github.com/sabban)
|
||||
* Fix `--assign-*` commands when using MySQL driver ([#45](https://github.com/moul/sshportal/issues/45))
|
||||
* Add *HOP* support, an efficient and integrated way of using a jump host transparently ([#47](https://github.com/moul/sshportal/issues/47)) by [@mathieui](https://github.com/mathieui)
|
||||
* Fix panic on some `ls` commands ([#54](https://github.com/moul/sshportal/pull/54)) by [@jle64](https://github.com/jle64)
|
||||
* Add tunnels (`direct-tcp`) support with logging ([#44](https://github.com/moul/sshportal/issues/44)) by [@sabban](https://github.com/sabban)
|
||||
* Add `key import` command ([#52](https://github.com/moul/sshportal/issues/52)) by [@adyxax](https://github.com/adyxax)
|
||||
* Add 'exec' logging ([#40](https://github.com/moul/sshportal/issues/40)) by [@sabban](https://github.com/sabban)
|
||||
|
||||
## v1.7.1 (2018-01-03)
|
||||
|
||||
* Return non-null exit-code on authentication error
|
||||
* **hotfix**: repair invite system (broken in v1.7.0)
|
||||
|
||||
## v1.7.0 (2018-01-02)
|
||||
|
||||
Breaking changes:
|
||||
@@ -12,6 +33,7 @@ Changes:
|
||||
* 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)
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -24,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:
|
||||
@@ -33,7 +33,7 @@ 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:
|
||||
|
||||
35
README.md
35
README.md
@@ -28,7 +28,8 @@ Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
|
||||
* Admin commands can be run directly or in an interactive shell
|
||||
* Host management
|
||||
* User management (invite, group, stats)
|
||||
* Host Key management (remote host key learning)
|
||||
* 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, ...)
|
||||
@@ -37,13 +38,22 @@ Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
|
||||
* Sensitive data encryption
|
||||
* Session management (see active connections, history, stats, stop)
|
||||
* Audit log (logging every user action)
|
||||
* Record TTY Session
|
||||
* Tunnels logging
|
||||
* Host Keys verifications shared across users
|
||||
* Healthcheck user (replying OK to any user)
|
||||
* ipv4 and ipv6 support
|
||||
* [`scp`](https://linux.die.net/man/1/scp) support
|
||||
* [`rsync`](https://linux.die.net/man/1/rsync) 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
|
||||
* 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
|
||||
|
||||
@@ -170,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
|
||||
@@ -186,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...
|
||||
@@ -228,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.7.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
|
||||
@@ -237,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.7.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
|
||||
@@ -245,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.7.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
|
||||
```
|
||||
@@ -403,4 +414,4 @@ This is totally experimental for now, so please file issues to let me know what
|
||||
|
||||
|
||||
## License
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fmoul%2Fsshportal?ref=badge_large)
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fmoul%2Fsshportal?ref=badge_large) [](https://www.guardrails.io)
|
||||
|
||||
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 == "" {
|
||||
|
||||
152
db.go
152
db.go
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -53,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
|
||||
@@ -156,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_-]*")
|
||||
|
||||
@@ -168,37 +179,113 @@ func init() {
|
||||
}))
|
||||
}
|
||||
|
||||
func (host *Host) URL() string {
|
||||
return fmt.Sprintf("%s@%s", host.User, host.Addr)
|
||||
}
|
||||
// Host helpers
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -217,7 +304,7 @@ func HostByName(db *gorm.DB, name string) (*Host, error) {
|
||||
|
||||
func (host *Host) clientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, error) {
|
||||
config := gossh.ClientConfig{
|
||||
User: host.User,
|
||||
User: host.Username(),
|
||||
HostKeyCallback: hk,
|
||||
Auth: []gossh.AuthMethod{},
|
||||
}
|
||||
@@ -228,8 +315,8 @@ func (host *Host) clientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, e
|
||||
}
|
||||
config.Auth = append(config.Auth, gossh.PublicKeys(signer))
|
||||
}
|
||||
if host.Password != "" {
|
||||
config.Auth = append(config.Auth, gossh.Password(host.Password))
|
||||
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)
|
||||
@@ -281,16 +368,11 @@ func (u *User) HasRole(name string) bool {
|
||||
return false
|
||||
}
|
||||
func (u *User) CheckRoles(names []string) error {
|
||||
ok := false
|
||||
for _, name := range names {
|
||||
if u.HasRole(name) {
|
||||
ok = true
|
||||
break
|
||||
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
|
||||
@@ -5,10 +5,18 @@ cp /integration/client_test_rsa ~/.ssh/id_rsa
|
||||
chmod -R 700 ~/.ssh
|
||||
cat >~/.ssh/config <<EOF
|
||||
Host sshportal
|
||||
UserKnownHostsFile /dev/null
|
||||
StrictHostKeyChecking no
|
||||
Port 2222
|
||||
HostName sshportal
|
||||
|
||||
Host testserver
|
||||
Port 2222
|
||||
HostName testserver
|
||||
|
||||
Host *
|
||||
StrictHostKeyChecking no
|
||||
ControlMaster auto
|
||||
SendEnv TEST_*
|
||||
|
||||
EOF
|
||||
|
||||
set -x
|
||||
@@ -39,6 +47,10 @@ 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
|
||||
@@ -51,3 +63,17 @@ ssh sshportal -l admin config backup --indent --ignore-events > backup-2
|
||||
set -xe
|
||||
diff backup-1.clean backup-2.clean
|
||||
)
|
||||
|
||||
if [ "$CIRCLECI" = "true" ]; then
|
||||
echo "Strage behavior with cross-container communication on CircleCI, skipping some tests..."
|
||||
else
|
||||
# bastion
|
||||
ssh sshportal -l admin host create --name=testserver toto@testserver:2222
|
||||
out="$(ssh sshportal -l testserver echo hello | head -n 1)"
|
||||
test "$out" = '{"User":"toto","Environ":null,"Command":["echo","hello"]}'
|
||||
|
||||
out="$(TEST_A=1 TEST_B=2 TEST_C=3 TEST_D=4 TEST_E=5 TEST_F=6 TEST_G=7 TEST_H=8 TEST_I=9 ssh sshportal -l testserver echo hello | head -n 1)"
|
||||
test "$out" = '{"User":"toto","Environ":["TEST_A=1","TEST_B=2","TEST_C=3","TEST_D=4","TEST_E=5","TEST_F=6","TEST_G=7","TEST_H=8","TEST_I=9"],"Command":["echo","hello"]}'
|
||||
fi
|
||||
|
||||
# TODO: test more cases (forwards, scp, sftp, interactive, pty, stdin, exit code, ...)
|
||||
|
||||
@@ -6,6 +6,14 @@ services:
|
||||
environment:
|
||||
- SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN=integration
|
||||
command: server --debug
|
||||
depends_on:
|
||||
- testserver
|
||||
ports:
|
||||
- 2222
|
||||
|
||||
testserver:
|
||||
image: moul/sshportal
|
||||
command: _test_server
|
||||
ports:
|
||||
- 2222
|
||||
|
||||
@@ -13,6 +21,7 @@ services:
|
||||
build: .
|
||||
depends_on:
|
||||
- sshportal
|
||||
- testserver
|
||||
#volumes:
|
||||
# - .:/integration
|
||||
tty: true
|
||||
|
||||
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)
|
||||
}
|
||||
158
main.go
158
main.go
@@ -1,14 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
@@ -16,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.7.0"
|
||||
Version = "1.8.0+dev"
|
||||
// GitTag will be overwritten automatically by the build system
|
||||
GitTag string
|
||||
// GitSha will be overwritten automatically by the build system
|
||||
@@ -40,9 +37,18 @@ func main() {
|
||||
app.Email = "https://github.com/moul/sshportal"
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "server",
|
||||
Usage: "Start sshportal server",
|
||||
Action: server,
|
||||
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",
|
||||
@@ -68,10 +74,15 @@ func main() {
|
||||
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: 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",
|
||||
@@ -87,6 +98,10 @@ func main() {
|
||||
Usage: "Do not print errors, if any",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: "_test_server",
|
||||
Hidden: true,
|
||||
Action: testServer,
|
||||
},
|
||||
}
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
@@ -94,133 +109,54 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if c.Bool("debug") {
|
||||
db.LogMode(true)
|
||||
return
|
||||
}
|
||||
db.LogMode(c.debug)
|
||||
if err = dbInit(db); err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
opts := []ssh.Option{}
|
||||
// custom PublicKeyAuth handler
|
||||
opts = append(opts, ssh.PublicKeyAuth(publicKeyAuthHandler(db, c)))
|
||||
opts = append(opts, ssh.PasswordAuth(passwordAuthHandler(db, c)))
|
||||
|
||||
// retrieve sshportal SSH private key from databse
|
||||
opts = append(opts, func(srv *ssh.Server) error {
|
||||
var key SSHKey
|
||||
if err = SSHKeysByIdentifiers(db, []string{"host"}).First(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
SSHKeyDecrypt(c.String("aes-key"), &key)
|
||||
|
||||
var signer gossh.Signer
|
||||
signer, err = gossh.ParsePrivateKey([]byte(key.PrivKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.AddHostKey(signer)
|
||||
return nil
|
||||
})
|
||||
|
||||
// create TCP listening socket
|
||||
ln, err := net.Listen("tcp", c.String("bind-address"))
|
||||
ln, err := net.Listen("tcp", c.bindAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// configure server
|
||||
srv := &ssh.Server{
|
||||
Addr: c.String("bind-address"),
|
||||
Addr: c.bindAddr,
|
||||
Handler: shellHandler, // ssh.Server.Handler is the handler for the DefaultSessionHandler
|
||||
Version: fmt.Sprintf("sshportal-%s", Version),
|
||||
ChannelHandler: channelHandler,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("info: SSH Server accepting connections on %s", c.String("bind-address"))
|
||||
log.Printf("info: SSH Server accepting connections on %s", c.bindAddr)
|
||||
return srv.Serve(ln)
|
||||
}
|
||||
|
||||
// perform a healthcheck test without requiring an ssh client or an ssh key (used for Docker's HEALTHCHECK)
|
||||
func healthcheck(c *cli.Context) error {
|
||||
config := gossh.ClientConfig{
|
||||
User: "healthcheck",
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key gossh.PublicKey) error { return nil },
|
||||
Auth: []gossh.AuthMethod{gossh.Password("healthcheck")},
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
for {
|
||||
if err := healthcheckOnce(c.String("addr"), config, c.Bool("quiet")); err != nil {
|
||||
if !c.Bool("quiet") {
|
||||
log.Printf("error: %v", err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := healthcheckOnce(c.String("addr"), config, c.Bool("quiet")); err != nil {
|
||||
if c.Bool("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
|
||||
}
|
||||
|
||||
@@ -3,60 +3,166 @@ 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 ChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, config Config) error {
|
||||
if newChan.ChannelType() != "session" {
|
||||
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
|
||||
}
|
||||
lch, lreqs, err := newChan.Accept()
|
||||
// TODO: defer clean closer
|
||||
if err != nil {
|
||||
// TODO: trigger event callback
|
||||
return nil
|
||||
}
|
||||
|
||||
// open client channel
|
||||
rconn, err := gossh.Dial("tcp", config.Addr, config.ClientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = rconn.Close() }()
|
||||
rch, rreqs, err := rconn.OpenChannel("session", []byte{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// pipe everything
|
||||
return pipe(lreqs, rreqs, lch, rch)
|
||||
}
|
||||
|
||||
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel) error {
|
||||
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()
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(lch, rch)
|
||||
errch <- errors.New("lch closed the connection")
|
||||
}()
|
||||
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()
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(rch, lch)
|
||||
errch <- errors.New("rch closed the connection")
|
||||
}()
|
||||
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 {
|
||||
@@ -65,6 +171,12 @@ func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel) error {
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
317
shell.go
317
shell.go
@@ -32,6 +32,10 @@ var banner = `
|
||||
`
|
||||
var startTime = time.Now()
|
||||
|
||||
const (
|
||||
naMessage = "n/a"
|
||||
)
|
||||
|
||||
func shell(s ssh.Session) error {
|
||||
var (
|
||||
sshCommand = s.Command()
|
||||
@@ -271,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
|
||||
@@ -286,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
|
||||
@@ -323,11 +339,11 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
for _, key := range config.SSHKeys {
|
||||
SSHKeyDecrypt(actx.globalContext.String("aes-key"), key)
|
||||
SSHKeyDecrypt(actx.config.aesKey, key)
|
||||
}
|
||||
if !c.Bool("decrypt") {
|
||||
for _, key := range config.SSHKeys {
|
||||
if err := SSHKeyEncrypt(actx.globalContext.String("aes-key"), key); err != nil {
|
||||
if err := SSHKeyEncrypt(actx.config.aesKey, key); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -337,11 +353,11 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
for _, host := range config.Hosts {
|
||||
HostDecrypt(actx.globalContext.String("aes-key"), host)
|
||||
HostDecrypt(actx.config.aesKey, host)
|
||||
}
|
||||
if !c.Bool("decrypt") {
|
||||
for _, host := range config.Hosts {
|
||||
if err := HostEncrypt(actx.globalContext.String("aes-key"), host); err != nil {
|
||||
if err := HostEncrypt(actx.config.aesKey, host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -456,9 +472,9 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
}
|
||||
for _, host := range config.Hosts {
|
||||
HostDecrypt(actx.globalContext.String("aes-key"), host)
|
||||
HostDecrypt(actx.config.aesKey, host)
|
||||
if !c.Bool("decrypt") {
|
||||
if err := HostEncrypt(actx.globalContext.String("aes-key"), host); err != nil {
|
||||
if err := HostEncrypt(actx.config.aesKey, host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -492,9 +508,9 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
}
|
||||
for _, sshKey := range config.SSHKeys {
|
||||
SSHKeyDecrypt(actx.globalContext.String("aes-key"), sshKey)
|
||||
SSHKeyDecrypt(actx.config.aesKey, sshKey)
|
||||
if !c.Bool("decrypt") {
|
||||
if err := SSHKeyEncrypt(actx.globalContext.String("aes-key"), sshKey); err != nil {
|
||||
if err := SSHKeyEncrypt(actx.config.aesKey, sshKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -635,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 {
|
||||
@@ -653,20 +670,29 @@ GLOBAL OPTIONS:
|
||||
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
|
||||
@@ -694,7 +720,7 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
|
||||
// encrypt
|
||||
if err := HostEncrypt(actx.globalContext.String("aes-key"), host); err != nil {
|
||||
if err := HostEncrypt(actx.config.aesKey, host); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -731,7 +757,7 @@ GLOBAL OPTIONS:
|
||||
|
||||
if c.Bool("decrypt") {
|
||||
for _, host := range hosts {
|
||||
HostDecrypt(actx.globalContext.String("aes-key"), host)
|
||||
HostDecrypt(actx.config.aesKey, host)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,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)
|
||||
@@ -790,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
|
||||
})
|
||||
}
|
||||
@@ -827,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`"},
|
||||
},
|
||||
@@ -855,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()
|
||||
@@ -864,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
|
||||
@@ -886,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
|
||||
@@ -1020,6 +1095,47 @@ GLOBAL OPTIONS:
|
||||
|
||||
return HostGroupsByIdentifiers(db, c.Args()).Delete(&HostGroup{}).Error
|
||||
},
|
||||
}, {
|
||||
Name: "update",
|
||||
Usage: "Updates a host group",
|
||||
ArgsUsage: "HOSTGROUP...",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{Name: "name", Usage: "Assigns a new name to the host group"},
|
||||
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 1 {
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var hostgroups []HostGroup
|
||||
if err := HostGroupsByIdentifiers(db, c.Args()).Find(&hostgroups).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(hostgroups) > 1 && c.String("name") != "" {
|
||||
return fmt.Errorf("cannot set --name when editing multiple hostgroups at once")
|
||||
}
|
||||
|
||||
tx := db.Begin()
|
||||
for _, hostgroup := range hostgroups {
|
||||
model := tx.Model(&hostgroup)
|
||||
// simple fields
|
||||
for _, fieldname := range []string{"name", "comment"} {
|
||||
if c.String(fieldname) != "" {
|
||||
if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tx.Commit().Error
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
@@ -1030,14 +1146,14 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(s, "Debug mode (server): %v\n", actx.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", actx.globalContext.Bool("demo"))
|
||||
fmt.Fprintf(s, "DB Driver: %s\n", actx.globalContext.String("db-driver"))
|
||||
fmt.Fprintf(s, "DB Conn: %s\n", actx.globalContext.String("db-conn"))
|
||||
fmt.Fprintf(s, "Bind Address: %s\n", actx.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)
|
||||
@@ -1083,8 +1199,8 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
|
||||
key, err := NewSSHKey(c.String("type"), c.Uint("length"))
|
||||
if actx.globalContext.String("aes-key") != "" {
|
||||
if err2 := SSHKeyEncrypt(actx.globalContext.String("aes-key"), key); err2 != nil {
|
||||
if actx.config.aesKey != "" {
|
||||
if err2 := SSHKeyEncrypt(actx.config.aesKey, key); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
}
|
||||
@@ -1107,6 +1223,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...",
|
||||
@@ -1129,7 +1299,7 @@ GLOBAL OPTIONS:
|
||||
|
||||
if c.Bool("decrypt") {
|
||||
for _, key := range keys {
|
||||
SSHKeyDecrypt(actx.globalContext.String("aes-key"), key)
|
||||
SSHKeyDecrypt(actx.config.aesKey, key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1238,7 +1408,7 @@ GLOBAL OPTIONS:
|
||||
if err := SSHKeysByIdentifiers(SSHKeysPreload(db), c.Args()).First(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
SSHKeyDecrypt(actx.globalContext.String("aes-key"), &key)
|
||||
SSHKeyDecrypt(actx.config.aesKey, &key)
|
||||
|
||||
type line struct {
|
||||
key string
|
||||
@@ -1513,11 +1683,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()
|
||||
@@ -1528,12 +1703,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
|
||||
},
|
||||
},
|
||||
@@ -1665,6 +1845,47 @@ GLOBAL OPTIONS:
|
||||
|
||||
return UserGroupsByIdentifiers(db, c.Args()).Delete(&UserGroup{}).Error
|
||||
},
|
||||
}, {
|
||||
Name: "update",
|
||||
Usage: "Updates a user group",
|
||||
ArgsUsage: "USERGROUP...",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{Name: "name", Usage: "Assigns a new name to the user group"},
|
||||
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 1 {
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var usergroups []UserGroup
|
||||
if err := UserGroupsByIdentifiers(db, c.Args()).Find(&usergroups).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(usergroups) > 1 && c.String("name") != "" {
|
||||
return fmt.Errorf("cannot set --name when editing multiple usergroups at once")
|
||||
}
|
||||
|
||||
tx := db.Begin()
|
||||
for _, usergroup := range usergroups {
|
||||
model := tx.Model(&usergroup)
|
||||
// simple fields
|
||||
for _, fieldname := range []string{"name", "comment"} {
|
||||
if c.String(fieldname) != "" {
|
||||
if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tx.Commit().Error
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
@@ -1781,9 +2002,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),
|
||||
@@ -1880,10 +2105,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,
|
||||
|
||||
166
ssh.go
166
ssh.go
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/moul/sshportal/pkg/bastionsession"
|
||||
"github.com/urfave/cli"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
@@ -27,7 +26,7 @@ type authContext struct {
|
||||
inputUsername string
|
||||
db *gorm.DB
|
||||
userKey UserKey
|
||||
globalContext *cli.Context
|
||||
config *configServe
|
||||
authMethod string
|
||||
authSuccess bool
|
||||
}
|
||||
@@ -87,8 +86,9 @@ func dynamicHostKey(db *gorm.DB, host *Host) gossh.HostKeyCallback {
|
||||
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
|
||||
// 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)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
|
||||
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, clientConfig, err := bastionConfig(ctx)
|
||||
host, err := HostByName(actx.db, actx.inputUsername)
|
||||
if err != nil {
|
||||
ch, _, err2 := newChan.Accept()
|
||||
if err2 != nil {
|
||||
@@ -112,79 +112,122 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
|
||||
return
|
||||
}
|
||||
|
||||
sess := Session{
|
||||
UserID: actx.user.ID,
|
||||
HostID: host.ID,
|
||||
Status: SessionStatusActive,
|
||||
}
|
||||
if err = actx.db.Create(&sess).Error; err != nil {
|
||||
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: %v\n", err)
|
||||
fmt.Fprintf(ch, "error: unknown bastion scheme: %q\n", host.Scheme())
|
||||
// FIXME: force close all channels
|
||||
_ = ch.Close()
|
||||
return
|
||||
}
|
||||
|
||||
err = bastionsession.ChannelHandler(srv, conn, newChan, ctx, bastionsession.Config{
|
||||
Addr: host.Addr,
|
||||
ClientConfig: clientConfig,
|
||||
})
|
||||
|
||||
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)
|
||||
default: // shell
|
||||
ssh.DefaultChannelHandler(srv, conn, newChan, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func bastionConfig(ctx ssh.Context) (*Host, *gossh.ClientConfig, error) {
|
||||
func bastionClientConfig(ctx ssh.Context, host *Host) (*gossh.ClientConfig, error) {
|
||||
actx := ctx.Value(authContextKey).(*authContext)
|
||||
|
||||
host, err := HostByName(actx.db, actx.inputUsername)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
clientConfig, err := host.clientConfig(dynamicHostKey(actx.db, host))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
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, nil, err
|
||||
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, nil, err
|
||||
return nil, err
|
||||
}
|
||||
action, err2 := CheckACLs(tmpUser, tmpHost)
|
||||
if err2 != nil {
|
||||
return nil, nil, err2
|
||||
return nil, err2
|
||||
}
|
||||
|
||||
HostDecrypt(actx.globalContext.String("aes-key"), host)
|
||||
SSHKeyDecrypt(actx.globalContext.String("aes-key"), host.SSHKey)
|
||||
HostDecrypt(actx.config.aesKey, host)
|
||||
SSHKeyDecrypt(actx.config.aesKey, host.SSHKey)
|
||||
|
||||
switch action {
|
||||
case ACLActionAllow:
|
||||
case ACLActionDeny:
|
||||
return nil, nil, fmt.Errorf("you don't have permission to that host")
|
||||
return nil, fmt.Errorf("you don't have permission to that host")
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("invalid ACL action: %q", action)
|
||||
return nil, fmt.Errorf("invalid ACL action: %q", action)
|
||||
}
|
||||
return host, clientConfig, nil
|
||||
return clientConfig, nil
|
||||
}
|
||||
|
||||
func shellHandler(s ssh.Session) {
|
||||
@@ -195,6 +238,7 @@ func shellHandler(s ssh.Session) {
|
||||
|
||||
if actx.err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", actx.err)
|
||||
_ = s.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -209,6 +253,7 @@ func shellHandler(s ssh.Session) {
|
||||
case UserTypeShell:
|
||||
if err := shell(s); err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
_ = s.Exit(1)
|
||||
}
|
||||
return
|
||||
case UserTypeInvite:
|
||||
@@ -218,12 +263,12 @@ func shellHandler(s ssh.Session) {
|
||||
panic("should not happen")
|
||||
}
|
||||
|
||||
func passwordAuthHandler(db *gorm.DB, globalContext *cli.Context) ssh.PasswordHandler {
|
||||
func passwordAuthHandler(db *gorm.DB, cfg *configServe) ssh.PasswordHandler {
|
||||
return func(ctx ssh.Context, pass string) bool {
|
||||
actx := &authContext{
|
||||
db: db,
|
||||
inputUsername: ctx.User(),
|
||||
globalContext: globalContext,
|
||||
config: cfg,
|
||||
authMethod: "password",
|
||||
}
|
||||
actx.authSuccess = actx.userType() == UserTypeHealthcheck
|
||||
@@ -232,12 +277,29 @@ func passwordAuthHandler(db *gorm.DB, globalContext *cli.Context) ssh.PasswordHa
|
||||
}
|
||||
}
|
||||
|
||||
func publicKeyAuthHandler(db *gorm.DB, globalContext *cli.Context) ssh.PublicKeyHandler {
|
||||
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(),
|
||||
globalContext: globalContext,
|
||||
config: cfg,
|
||||
authMethod: "pubkey",
|
||||
authSuccess: true,
|
||||
}
|
||||
@@ -247,14 +309,14 @@ func publicKeyAuthHandler(db *gorm.DB, globalContext *cli.Context) ssh.PublicKey
|
||||
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() == "invite" {
|
||||
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() == "invite" {
|
||||
if actx.userType() == UserTypeInvite {
|
||||
inputToken := strings.Split(actx.inputUsername, ":")[1]
|
||||
if len(inputToken) > 0 {
|
||||
db.Where("invite_token = ?", inputToken).First(&actx.user)
|
||||
@@ -266,11 +328,11 @@ func publicKeyAuthHandler(db *gorm.DB, globalContext *cli.Context) ssh.PublicKey
|
||||
Comment: "created by sshportal",
|
||||
AuthorizedKey: string(gossh.MarshalAuthorizedKey(key)),
|
||||
}
|
||||
db.Create(actx.userKey)
|
||||
db.Create(&actx.userKey)
|
||||
|
||||
// token is only usable once
|
||||
actx.user.InviteToken = ""
|
||||
db.Model(actx.user).Updates(actx.user)
|
||||
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 {
|
||||
|
||||
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()
|
||||
}
|
||||
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)
|
||||
}
|
||||
6
vendor/github.com/reiver/go-telnet/writer.go
generated
vendored
Normal file
6
vendor/github.com/reiver/go-telnet/writer.go
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
package telnet
|
||||
|
||||
|
||||
type Writer interface {
|
||||
Write([]byte) (int, error)
|
||||
}
|
||||
30
vendor/vendor.json
vendored
30
vendor/vendor.json
vendored
@@ -8,6 +8,12 @@
|
||||
"revision": "648efa622239a2f6ff949fed78ee37b48d499ba4",
|
||||
"revisionTime": "2016-10-02T11:37:05Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "MHJo0MQ1wV3xSm0ncSn/aHaZR3Y=",
|
||||
"path": "github.com/arkan/bastion/pkg/logchannel",
|
||||
"revision": "0eb93ed2121907205ca69f46667a25f8b4320fde",
|
||||
"revisionTime": "2018-01-04T15:54:52Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "qe14CYEIsrbHmel1u0gsdZNFPLQ=",
|
||||
"path": "github.com/asaskevich/govalidator",
|
||||
@@ -20,12 +26,6 @@
|
||||
"revision": "7a41df006ff9af79a29f0ffa9c5f21fbe6314a2d",
|
||||
"revisionTime": "2017-01-10T07:11:07Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "XIIHyP5QkRbX/zBNqq+hAQ2SmzE=",
|
||||
"path": "github.com/gliderlabs/ssh",
|
||||
"revision": "ce31f3cc47feee0c38db7ecfaa154026929ffbda",
|
||||
"revisionTime": "2017-12-06T20:46:25Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "fI9spYCCgBl19GMD/JsW+znBHkw=",
|
||||
"path": "github.com/go-gormigrate/gormigrate",
|
||||
@@ -62,6 +62,12 @@
|
||||
"revision": "1c35d901db3da928c72a72d8458480cc9ade058f",
|
||||
"revisionTime": "2017-01-02T12:52:26Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "gGDSJToIqPYPEnKst2qLfuTeIZU=",
|
||||
"path": "github.com/kr/pty",
|
||||
"revision": "2c10821df3c3cf905230d078702dfbe9404c9b23",
|
||||
"revisionTime": "2017-03-07T14:53:09Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "6WOrEA4SH+M4UPESadwZ7J4ytnE=",
|
||||
"path": "github.com/mattn/go-colorable",
|
||||
@@ -104,6 +110,18 @@
|
||||
"revision": "a7a4c189eb47ed33ce7b35f2880070a0c82a67d4",
|
||||
"revisionTime": "2017-09-25T23:40:30Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "7BsTyiYZMTvIGT/KsF9a3tAoN0g=",
|
||||
"path": "github.com/reiver/go-oi",
|
||||
"revision": "431c83978379297f04f85f6eb94f129f25ab741d",
|
||||
"revisionTime": "2016-03-25T06:16:15Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "sb1kOSRpAu22ovV0dXHkPlG06i4=",
|
||||
"path": "github.com/reiver/go-telnet",
|
||||
"revision": "6b696f32801a8f8dd07947f1e1fdb1a7dc4766ff",
|
||||
"revisionTime": "2016-03-30T05:09:16Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "zRAcxJW8WNDMryOBS4VE03V6ivw=",
|
||||
"path": "github.com/urfave/cli",
|
||||
|
||||
Reference in New Issue
Block a user