Compare commits
387 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
614418e7be | ||
|
|
a5bade8761 | ||
|
|
7404704bfe | ||
|
|
84a0a31eda | ||
|
|
40bbea590c | ||
|
|
e455d50db9 | ||
|
|
be3f215e24 | ||
|
|
c290253546 | ||
|
|
28a5fd1846 | ||
|
|
19605f0054 | ||
|
|
5b4332072c | ||
|
|
c1c4c556b4 | ||
|
|
3c32177213 | ||
|
|
762736d622 | ||
|
|
bbbc484fe8 | ||
|
|
e1602364c8 | ||
|
|
2540d1e861 | ||
|
|
177a198420 | ||
|
|
51612aab13 | ||
|
|
e20af1dde5 | ||
|
|
6caa1f1657 | ||
|
|
e0f76d15ec | ||
|
|
05225a4b25 | ||
|
|
bcc150727f | ||
|
|
9062417d13 | ||
|
|
baeade4043 | ||
|
|
b9552e98b5 | ||
|
|
715ccde829 | ||
|
|
f5dc1bd1b9 | ||
|
|
c79c50aeb6 | ||
|
|
df3542c6ee | ||
|
|
e40f5307a3 | ||
|
|
6e6045306b | ||
|
|
874467b1e6 | ||
|
|
5c1c559a9a | ||
|
|
6872c727ef | ||
|
|
cae996d041 | ||
|
|
a23b77282c | ||
|
|
24814c4152 | ||
|
|
07359988d0 | ||
|
|
db6eb63297 | ||
|
|
5fdb31b97d | ||
|
|
bce6b1998b | ||
|
|
f7fa60da97 | ||
|
|
d2cd6b64a3 | ||
|
|
1ef0cc8725 | ||
|
|
d894005c3f | ||
|
|
af7206d114 | ||
|
|
1f9d962cd6 | ||
|
|
460041c6e3 | ||
|
|
7068565ab1 | ||
|
|
74bd885c1d | ||
|
|
9317f206d1 | ||
|
|
6c3f803dc6 | ||
|
|
9c3d29eb83 | ||
|
|
e339a73931 | ||
|
|
0dcab1b380 | ||
|
|
032f802348 | ||
|
|
7fd9be9058 | ||
|
|
83b54aeeff | ||
|
|
2323d6fd1e | ||
|
|
4c947ce391 | ||
|
|
44559f0547 | ||
|
|
8234119cd4 | ||
|
|
7a75c13ac4 | ||
|
|
4b10131790 | ||
|
|
a29c6e8338 | ||
|
|
198e0717b5 | ||
|
|
d8fa2f6925 | ||
|
|
16c8c0092e | ||
|
|
b0dfff2d90 | ||
|
|
9d2badf253 | ||
|
|
428344da17 | ||
|
|
0c07ac790a | ||
|
|
365a37959a | ||
|
|
90fd6057cf | ||
|
|
4220f3fb89 | ||
|
|
3e2acfc992 | ||
|
|
9c464b2610 | ||
|
|
5760aece65 | ||
|
|
a24e20252a | ||
|
|
37a7fa1917 | ||
|
|
f1b28b0363 | ||
|
|
e43bb55e70 | ||
|
|
763ced7524 | ||
|
|
54128beb12 | ||
|
|
64ba179cc7 | ||
|
|
bbdb4851a5 | ||
|
|
63719ec00e | ||
|
|
0722497336 | ||
|
|
e74f7221b5 | ||
|
|
f4fc3a90bc | ||
|
|
df3aa6e165 | ||
|
|
986bcd7971 | ||
|
|
7f3ea431a1 | ||
|
|
dae0252857 | ||
|
|
33b8e5272c | ||
|
|
21e73757ac | ||
|
|
bcb5d3b7ef | ||
|
|
d2f3f460b2 | ||
|
|
e06fe6f5a3 | ||
|
|
fb9dabfe6b | ||
|
|
0e0cd8fed5 | ||
|
|
8959e1782f | ||
|
|
33151105e0 | ||
|
|
77b40eb9ed | ||
|
|
075dfd0aa7 | ||
|
|
5cf6b1c218 | ||
|
|
6527746a91 | ||
|
|
020ca9c6b3 | ||
|
|
8c7831480b | ||
|
|
e399dfd8e4 | ||
|
|
be83c7148d | ||
|
|
ce187e8675 | ||
|
|
f13ede4ba7 | ||
|
|
fb061ed419 | ||
|
|
b4a377f269 | ||
|
|
de6f37aa64 | ||
|
|
32219577b8 | ||
|
|
abc7329a71 | ||
|
|
675942e967 | ||
|
|
5b20cd501e | ||
|
|
b6aaf4d7cf | ||
|
|
972e232559 | ||
|
|
851a91b1a0 | ||
|
|
6a068dc430 | ||
|
|
2cdfcf60fe | ||
|
|
5d9e0c367a | ||
|
|
cbf8263033 | ||
|
|
846c73d9bc | ||
|
|
e0b43b1976 | ||
|
|
6a6e788968 | ||
|
|
4754cad42a | ||
|
|
db58e53f3b | ||
|
|
b31acb4348 | ||
|
|
c794c2c076 | ||
|
|
42d6cd44bb | ||
|
|
f9057ca56a | ||
|
|
c2f1999037 | ||
|
|
44b386f7a7 | ||
|
|
89b296db4e | ||
|
|
c16403fb3f | ||
|
|
5e21fb72e6 | ||
|
|
c5681bf880 | ||
|
|
db85d6545d | ||
|
|
9912c3deba | ||
|
|
fc5c342e40 | ||
|
|
60707b3faa | ||
|
|
f36845ac6b | ||
|
|
9f76bd6cad | ||
|
|
c53d5d9964 | ||
|
|
171d461ea5 | ||
|
|
b2b04a1155 | ||
|
|
671ba03b78 | ||
|
|
9095725778 | ||
|
|
8b2e5daba3 | ||
|
|
75b7a5f571 | ||
|
|
4b9e881ad0 | ||
|
|
59f8f52cca | ||
|
|
4adaf83fd3 | ||
|
|
84464a4ea6 | ||
|
|
cafac0b8b5 | ||
|
|
5346300a64 | ||
|
|
1d4554eabc | ||
|
|
50bdba8b70 | ||
|
|
8c785f6dea | ||
|
|
93e6abc9ba | ||
|
|
60d7c85c11 | ||
|
|
883bad2ee5 | ||
|
|
7d68e144f8 | ||
|
|
7f32e38cf8 | ||
|
|
43a96d1636 | ||
|
|
00e7d2e45d | ||
|
|
2e711c3591 | ||
|
|
5d147fc03b | ||
|
|
3dccefbbcb | ||
|
|
7c4995fa4a | ||
|
|
2b8f051414 | ||
|
|
4e17c81d63 | ||
|
|
8b4b677d6a | ||
|
|
47229bf473 | ||
|
|
ec5b567da9 | ||
|
|
03b59fae1c | ||
|
|
ede8b3ecf2 | ||
|
|
7ae90b9199 | ||
|
|
a651da451e | ||
|
|
f220af5c54 | ||
|
|
eebf987900 | ||
|
|
3d5101011f | ||
|
|
2cdc19dfdd | ||
|
|
d8a7b1e16c | ||
|
|
d7490d089c | ||
|
|
774c6c0f64 | ||
|
|
92d11c53de | ||
|
|
c509f65a27 | ||
|
|
20b9e839d3 | ||
|
|
6f4fb24cd0 | ||
|
|
23a89fe1de | ||
|
|
559df1f523 | ||
|
|
ad2b8ebc38 | ||
|
|
3824629d4d | ||
|
|
38224714e1 | ||
|
|
a9f4227bba | ||
|
|
06bde77f51 | ||
|
|
2a2554e7a3 | ||
|
|
5d835011e6 | ||
|
|
0f294cd62d | ||
|
|
8e62d21c25 | ||
|
|
2a5dd63e87 | ||
|
|
06cb424b8f | ||
|
|
7c5864a9c3 | ||
|
|
668e34ccab | ||
|
|
95477715fc | ||
|
|
ecc004a485 | ||
|
|
9f0657374b | ||
|
|
61b7f72e94 | ||
|
|
db000baaa5 | ||
|
|
a1a3a29d00 | ||
|
|
2ea73a941f | ||
|
|
e860b60d20 | ||
|
|
d6be01b9b7 | ||
|
|
64c8e01c33 | ||
|
|
acce797e55 | ||
|
|
175fc8d68b | ||
|
|
b9d1cf69c7 | ||
|
|
41eeb364f8 | ||
|
|
a22f8f0b7b | ||
|
|
bd1c3609a7 | ||
|
|
c5e75df64f | ||
|
|
6b181dd291 | ||
|
|
4ab88cad10 | ||
|
|
b902953df4 | ||
|
|
e141368734 | ||
|
|
980da40988 | ||
|
|
22d25f1e70 | ||
|
|
84d77d0a9f | ||
|
|
b0afdf933a | ||
|
|
e9eef9a49e | ||
|
|
6f2b58cbdc | ||
|
|
09ac2c35f3 | ||
|
|
47a6fc9906 | ||
|
|
c3d49fde95 | ||
|
|
ec1e4d5c8a | ||
|
|
e65ef7ccc1 | ||
|
|
68e7fd2090 | ||
|
|
b958f8461f | ||
|
|
a08d84e7ed | ||
|
|
2b66d8d56a | ||
|
|
a40789e1f2 | ||
|
|
63571af252 | ||
|
|
75c6840ecd | ||
|
|
e6a02a85f0 | ||
|
|
2c3de75f3d | ||
|
|
7c4aab34ed | ||
|
|
a8480f82e0 | ||
|
|
a5dacca9a1 | ||
|
|
31ba233b34 | ||
|
|
5720123576 | ||
|
|
9cc09b320d | ||
|
|
cb3c1056e5 | ||
|
|
82f96e457c | ||
|
|
062e2b4b8f | ||
|
|
9de51acbcc | ||
|
|
6d3a97cdbc | ||
|
|
3ebcdd9c3d | ||
|
|
a9f86d1d01 | ||
|
|
2a68fc3114 | ||
|
|
2352a53e6e | ||
|
|
fcc94c58d9 | ||
|
|
da9c4920ab | ||
|
|
0295eedb6e | ||
|
|
7f26cc1dbb | ||
|
|
9e1c395810 | ||
|
|
9db4b92d4e | ||
|
|
ff46ee89d9 | ||
|
|
b9af077ef4 | ||
|
|
b23ee4144d | ||
|
|
57f894bfca | ||
|
|
58e2abca8c | ||
|
|
ed676b0d7e | ||
|
|
ed42f343d2 | ||
|
|
2555c478b4 | ||
|
|
6152e55e7d | ||
|
|
023cdd1bb3 | ||
|
|
5efe250466 | ||
|
|
695ddc91dd | ||
|
|
7b30017a14 | ||
|
|
e5542ae266 | ||
|
|
d19b8a53f2 | ||
|
|
2e39f70cd5 | ||
|
|
26c0bb8b1a | ||
|
|
12b0db07da | ||
|
|
7aace9109a | ||
|
|
6c4caea26f | ||
|
|
13c0726849 | ||
|
|
1b52673603 | ||
|
|
7ea7237d19 | ||
|
|
d6bb5e44a1 | ||
|
|
072464928b | ||
|
|
4125bc2768 | ||
|
|
ee29310ed3 | ||
|
|
0e6917ae2a | ||
|
|
57dd2c6c01 | ||
|
|
6494e69632 | ||
|
|
d6ea80dab1 | ||
|
|
fbb3e7134f | ||
|
|
9fdb36c6ca | ||
|
|
9bc545b4bb | ||
|
|
457f60f815 | ||
|
|
78db26a532 | ||
|
|
fb15225c35 | ||
|
|
c8fb103762 | ||
|
|
585fd3a3ff | ||
|
|
0aefd4d093 | ||
|
|
5f0c5b3375 | ||
|
|
6b0b22cb7b | ||
|
|
0f84be8fa0 | ||
|
|
849a485621 | ||
|
|
a721d4ff01 | ||
|
|
62c8fe2dbf | ||
|
|
756c8f02e8 | ||
|
|
62db91b7be | ||
|
|
8543a1f01a | ||
|
|
db20c81a39 | ||
|
|
395827afeb | ||
|
|
8329fd3ab7 | ||
|
|
e32f4200d1 | ||
|
|
7ed60f6908 | ||
|
|
a413aa86c2 | ||
|
|
b51c90a0e9 | ||
|
|
7245508c76 | ||
|
|
905159f044 | ||
|
|
ac8181474c | ||
|
|
554937dd7a | ||
|
|
4aa9a227e8 | ||
|
|
6e4cbf5dd8 | ||
|
|
44d1ac7f11 | ||
|
|
1c32da7751 | ||
|
|
999b740df6 | ||
|
|
6864b7ca10 | ||
|
|
546b350a6c | ||
|
|
d70296cd95 | ||
|
|
10f4ad49d9 | ||
|
|
edb230b278 | ||
|
|
efbf66a0a4 | ||
|
|
0746458762 | ||
|
|
f2738e2bd1 | ||
|
|
b0d8180809 | ||
|
|
f9d450ffaf | ||
|
|
391a39d82c | ||
|
|
7eb76c861f | ||
|
|
cd437a3a7b | ||
|
|
2accc7abd4 | ||
|
|
3c10578584 | ||
|
|
511470087b | ||
|
|
017ee2ab39 | ||
|
|
b093f61fb5 | ||
|
|
bd158819d3 | ||
|
|
86f6e87efe | ||
|
|
e377cac8e6 | ||
|
|
0fbcc0dd41 | ||
|
|
1fdf37dc07 | ||
|
|
4cf73e3410 | ||
|
|
328bb0153b | ||
|
|
1ddd6867b6 | ||
|
|
2becd5eec2 | ||
|
|
571b37da6b | ||
|
|
01d464f4c5 | ||
|
|
bf184c621d | ||
|
|
f4309f843b | ||
|
|
cbdc231cbf | ||
|
|
0f0a8dd9bb | ||
|
|
4189eb8154 | ||
|
|
1d6349767d | ||
|
|
f6ba06298d | ||
|
|
31a8cef59f | ||
|
|
beeba0551b | ||
|
|
a36bb68957 | ||
|
|
9cd9152a91 | ||
|
|
09c1e0504e | ||
|
|
37d7c839dd | ||
|
|
8ba418308e | ||
|
|
cfcf124d83 | ||
|
|
ccb0071d12 | ||
|
|
681f59c1e6 | ||
|
|
1bdee1a107 | ||
|
|
a2f3a51fe5 |
BIN
.assets/bastion.jpg
Normal file
|
After Width: | Height: | Size: 249 KiB |
BIN
.assets/client.gif
Normal file
|
After Width: | Height: | Size: 147 KiB |
30
.assets/cluster-mysql.dot
Normal file
@@ -0,0 +1,30 @@
|
||||
graph {
|
||||
rankdir=LR;
|
||||
subgraph cluster_sshportal {
|
||||
label="sshportal cluster";
|
||||
edge[style=dashed,color=grey,constraint=false];
|
||||
sshportal1; sshportal2; sshportal3; sshportalN;
|
||||
sshportal1 -- MySQL;
|
||||
sshportal2 -- MySQL;
|
||||
sshportal3 -- MySQL;
|
||||
sshportalN -- MySQL;
|
||||
}
|
||||
|
||||
subgraph cluster_hosts {
|
||||
label="hosts";
|
||||
host1; host2; host3; hostN;
|
||||
}
|
||||
|
||||
subgraph cluster_users {
|
||||
label="users";
|
||||
user1; user2; user3; userN;
|
||||
}
|
||||
|
||||
{
|
||||
user1 -- sshportal1 -- host1[color=red,penwidth=3.0];
|
||||
user2 -- sshportal2 -- host2[color=green,penwidth=3.0];
|
||||
user3 -- sshportal3 -- host3[color=blue,penwidth=3.0];
|
||||
user3 -- sshportal2 -- host1[color=purple,penwidth=3.0];
|
||||
userN -- sshportalN -- hostN[style=dotted];
|
||||
}
|
||||
}
|
||||
BIN
.assets/cluster-mysql.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
1
.assets/cluster-mysql.svg
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
.assets/demo.gif
Normal file
|
After Width: | Height: | Size: 171 KiB |
65
.assets/flow-diagram.dot
Normal file
@@ -0,0 +1,65 @@
|
||||
digraph {
|
||||
node[shape=record;style=rounded;fontname="helvetica-bold"];
|
||||
graph[layout=dot;rankdir=LR;overlap=prism;splines=true;fontname="helvetica-bold"];
|
||||
edge[arrowhead=none;fontname="helvetica"];
|
||||
|
||||
start[label="\$\> ssh sshportal";color=blue;fontcolor=blue;fontsize=18];
|
||||
|
||||
subgraph cluster_sshportal {
|
||||
graph[fontsize=18;color=gray;fontcolor=black];
|
||||
label="sshportal";
|
||||
{
|
||||
node[color=darkorange;fontcolor=darkorange];
|
||||
known_user_key[label="known user key"];
|
||||
unknown_user_key[label="unknown user key"];
|
||||
invite_manager[label="invite manager"];
|
||||
acl_manager[label="ACL manager"];
|
||||
}
|
||||
{
|
||||
node[color=darkgreen;fontcolor=darkgreen];
|
||||
builtin_shell[label="built-in\nconfig shell"];
|
||||
ssh_proxy[label="SSH proxy\nJump-Host"];
|
||||
learn_key[label="learn the\npub key"];
|
||||
}
|
||||
err_and_exit[label="\nerror\nand exit\n\n";color=red;fontcolor=red];
|
||||
{ rank=same; ssh_proxy; builtin_shell; learn_key; err_and_exit; }
|
||||
{ rank=same; known_user_key; unknown_user_key; }
|
||||
}
|
||||
|
||||
subgraph cluster_hosts {
|
||||
label="your hosts";
|
||||
graph[fontsize=18;color=gray;fontcolor=black];
|
||||
node[color=blue;fontcolor=blue];
|
||||
|
||||
host_1[label="root@host1"];
|
||||
host_2[label="user@host2:2222"];
|
||||
host_3[label="root@host3:1234"];
|
||||
}
|
||||
|
||||
{
|
||||
edge[color=blue];
|
||||
start -> known_user_key;
|
||||
start -> unknown_user_key;
|
||||
ssh_proxy -> host_1;
|
||||
ssh_proxy -> host_2;
|
||||
ssh_proxy -> host_3;
|
||||
}
|
||||
{
|
||||
edge[color=darkgreen;fontcolor=darkgreen];
|
||||
known_user_key -> builtin_shell[label="user=admin"];
|
||||
acl_manager -> ssh_proxy[label="authorized"];
|
||||
invite_manager -> learn_key[label="valid token"];
|
||||
}
|
||||
{
|
||||
edge[color=darkorange;fontcolor=darkorange];
|
||||
known_user_key -> acl_manager[label="user matches an existing host"];
|
||||
unknown_user_key -> invite_manager[label="user=invite:<token>";labelloc=b];
|
||||
}
|
||||
{
|
||||
edge[color=red;fontcolor=red];
|
||||
known_user_key -> err_and_exit[label="invalid user"];
|
||||
acl_manager -> err_and_exit[label="unauthorized"];
|
||||
unknown_user_key -> err_and_exit[label="any other user";constraint=false];
|
||||
invite_manager -> err_and_exit[label="invalid token";constraint=false];
|
||||
}
|
||||
}
|
||||
BIN
.assets/flow-diagram.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
1
.assets/flow-diagram.svg
Normal file
|
After Width: | Height: | Size: 12 KiB |
36
.assets/overview.dot
Normal file
@@ -0,0 +1,36 @@
|
||||
graph {
|
||||
rankdir=LR;
|
||||
node[shape=box,style=rounded,style=rounded,fillcolor=gray];
|
||||
|
||||
|
||||
subgraph cluster_sshportal {
|
||||
sshportal[penwidth=3.0,color=brown,fontcolor=brown,fontsize=20];
|
||||
shell[label="built-in\nadmin shell",color=orange,fontcolor=orange];
|
||||
db[color=gray,fontcolor=gray,shape=circle];
|
||||
{ rank=same; db; sshportal; shell }
|
||||
}
|
||||
|
||||
{
|
||||
node[color="green"];
|
||||
host1; host2; host3; hostN;
|
||||
}
|
||||
|
||||
{
|
||||
node[color="blue"];
|
||||
user1; user2; user3; userN;
|
||||
}
|
||||
|
||||
{
|
||||
edge[penwidth=3.0];
|
||||
user1 -- sshportal -- host1[color=red];
|
||||
user2 -- sshportal -- host2[color=blue];
|
||||
user3 -- sshportal -- host1[color=purple];
|
||||
user2 -- sshportal -- host3[color=green];
|
||||
user2 -- sshportal -- shell[color=orange,constraint=false];
|
||||
}
|
||||
|
||||
userN -- sshportal[style=dotted];
|
||||
sshportal -- hostN[style=dotted];
|
||||
sshportal -- db[style=dotted,color=grey];
|
||||
|
||||
}
|
||||
BIN
.assets/overview.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
1
.assets/overview.svg
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
.assets/server.gif
Normal file
|
After Width: | Height: | Size: 43 KiB |
1
.assets/sql-schema.svg
Normal file
|
After Width: | Height: | Size: 34 KiB |
40
.circleci/config.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
defaults: &defaults
|
||||
working_directory: /go/src/moul.io/sshportal
|
||||
docker:
|
||||
- image: circleci/golang:1.16.2
|
||||
environment:
|
||||
GO111MODULE: "on"
|
||||
|
||||
install_retry: &install_retry
|
||||
run:
|
||||
name: install retry
|
||||
command: |
|
||||
command -v wget &>/dev/null && wget -O /tmp/retry "https://github.com/moul/retry/releases/download/v0.5.0/retry_$(uname -s)_$(uname -m)" || true
|
||||
if [ ! -f /tmp/retry ]; then command -v curl &>/dev/null && curl -L -o /tmp/retry "https://github.com/moul/retry/releases/download/v0.5.0/retry_$(uname -s)_$(uname -m)"; fi
|
||||
chmod +x /tmp/retry
|
||||
/tmp/retry --version
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
docker.integration:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install Docker Compose
|
||||
command: |
|
||||
umask 022
|
||||
curl -L https://github.com/docker/compose/releases/download/1.11.4/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: true
|
||||
version: 18.09.3 # https://github.com/golang/go/issues/40893
|
||||
- *install_retry
|
||||
- run: /tmp/retry -m 3 docker build -t moul/sshportal .
|
||||
- run: /tmp/retry -m 3 make integration
|
||||
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_integration:
|
||||
jobs:
|
||||
- docker.integration
|
||||
@@ -1 +1,6 @@
|
||||
examples
|
||||
# .git/ # should be kept for git-based versionning
|
||||
|
||||
examples/
|
||||
.circleci/
|
||||
.assets/
|
||||
/sshportal
|
||||
|
||||
17
.gitattributes
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# Collapse vendored and generated files on GitHub
|
||||
AUTHORS linguist-generated
|
||||
vendor/* linguist-vendored
|
||||
rules.mk linguist-vendored
|
||||
*/vendor/* linguist-vendored
|
||||
*.gen.* linguist-generated
|
||||
*.pb.go linguist-generated
|
||||
*.pb.gw.go linguist-generated
|
||||
go.sum linguist-generated
|
||||
go.mod linguist-generated
|
||||
gen.sum linguist-generated
|
||||
|
||||
# Reduce conflicts on markdown files
|
||||
*.md merge=union
|
||||
6
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
github: ["moul"]
|
||||
patreon: moul
|
||||
open_collective: sshportal
|
||||
custom:
|
||||
- "https://www.buymeacoffee.com/moul"
|
||||
- "https://manfred.life/donate"
|
||||
25
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<!-- Thanks for filling an issue!
|
||||
|
||||
If this is a BUG REPORT, please:
|
||||
- Fill in as much of the template below as you can
|
||||
|
||||
If this is a FEATURE REQUEST, please:
|
||||
- Describe *in detail* the feature/behavior/change you would like to see
|
||||
-->
|
||||
|
||||
**What happened**:
|
||||
|
||||
**What you expected to happen**:
|
||||
|
||||
**How to reproduce it (as minimally and precisely as possible)**:
|
||||
|
||||
**Anything else we need to know?**:
|
||||
|
||||
<!--
|
||||
**Environment**:
|
||||
- sshportal --version
|
||||
- ssh sshportal info
|
||||
- OS (e.g. from /etc/os-release):
|
||||
- install method (e.g. go/docker/brew/...):
|
||||
- others:
|
||||
-->
|
||||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<!-- Thanks for sending a pull request! Here are some tips for you -->
|
||||
|
||||
**What this PR does / why we need it**:
|
||||
|
||||
**Which issue this PR fixes**: fixes #xxx, fixes #xxx...
|
||||
|
||||
**Special notes for your reviewer**:
|
||||
20
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: docker
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: gomod
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
7
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"groupName": "all",
|
||||
"gomodTidy": true
|
||||
}
|
||||
86
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the Docker image
|
||||
run: docker build . --file Dockerfile
|
||||
golangci-lint:
|
||||
name: golangci-lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: lint
|
||||
uses: golangci/golangci-lint-action@v2.5.1
|
||||
with:
|
||||
version: v1.28
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tests-on-windows:
|
||||
needs: golangci-lint # run after golangci-lint action to not produce duplicated errors
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
golang:
|
||||
- 1.15.0
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.golang }}
|
||||
- name: Run tests on Windows
|
||||
run: make.exe unittest
|
||||
continue-on-error: true
|
||||
tests-on-mac:
|
||||
needs: golangci-lint # run after golangci-lint action to not produce duplicated errors
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
golang:
|
||||
- 1.15.0
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.golang }}
|
||||
- uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ matrix.golang }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ matrix.golang }}-
|
||||
- name: Run tests on Unix-like operating systems
|
||||
run: make unittest
|
||||
tests-on-linux:
|
||||
needs: golangci-lint # run after golangci-lint action to not produce duplicated errors
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
golang:
|
||||
- 1.13
|
||||
- 1.14
|
||||
- 1.15.0
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.golang }}
|
||||
- uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ matrix.golang }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ matrix.golang }}-
|
||||
- name: Run tests on Unix-like operating systems
|
||||
run: make unittest
|
||||
13
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Semantic Release
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
semantic-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: codfish/semantic-release-action@v1
|
||||
if: github.ref == 'refs/heads/master'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
11
.gitignore
vendored
@@ -1,3 +1,12 @@
|
||||
coverage.txt
|
||||
dist/
|
||||
*~
|
||||
*#
|
||||
.*#
|
||||
.DS_Store
|
||||
/log/
|
||||
/sshportal
|
||||
*.db
|
||||
/data
|
||||
/data
|
||||
sshportal.history
|
||||
.idea
|
||||
|
||||
55
.golangci.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
run:
|
||||
deadline: 1m
|
||||
tests: false
|
||||
skip-files:
|
||||
- "testing.go"
|
||||
- ".*\\.pb\\.go"
|
||||
- ".*\\.gen\\.go"
|
||||
|
||||
linters-settings:
|
||||
golint:
|
||||
min-confidence: 0
|
||||
maligned:
|
||||
suggest-new: true
|
||||
goconst:
|
||||
min-len: 5
|
||||
min-occurrences: 4
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- depguard
|
||||
- dogsled
|
||||
#- dupl
|
||||
- errcheck
|
||||
#- funlen
|
||||
- gochecknoinits
|
||||
#- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
#- gocyclo
|
||||
- gofmt
|
||||
- goimports
|
||||
- golint
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
#- maligned
|
||||
- misspell
|
||||
- nakedret
|
||||
- prealloc
|
||||
- scopelint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
#- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
||||
29
.goreleaser.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
builds:
|
||||
-
|
||||
goos: [linux, darwin]
|
||||
goarch: [386, amd64, arm, arm64]
|
||||
ldflags:
|
||||
- -s -w -X main.GitSha={{.ShortCommit}} -X main.GitBranch=master -X main.GitTag={{.Version}}
|
||||
archives:
|
||||
- wrap_in_directory: true
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
brews:
|
||||
-
|
||||
name: sshportal
|
||||
github:
|
||||
owner: moul
|
||||
name: homebrew-moul
|
||||
commit_author:
|
||||
name: moul-bot
|
||||
email: "m+bot@42.am"
|
||||
homepage: https://manfred.life/sshportal
|
||||
description: "Simple, fun and transparent SSH (and telnet) bastion"
|
||||
8
.releaserc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
branch: 'master',
|
||||
plugins: [
|
||||
'@semantic-release/commit-analyzer',
|
||||
'@semantic-release/release-notes-generator',
|
||||
'@semantic-release/github',
|
||||
],
|
||||
};
|
||||
35
AUTHORS
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
# This file lists all individuals having contributed content to the repository.
|
||||
# For how it is generated, see 'https://github.com/moul/rules.mk'
|
||||
|
||||
ahh <ahamidullah@gmail.com>
|
||||
Alen Masic <alenn.masic@gmail.com>
|
||||
Alexander Turner <me@alexturner.co>
|
||||
bozzo <bozzo@users.noreply.github.com>
|
||||
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
|
||||
fossabot <badges@fossa.io>
|
||||
ImgBotApp <ImgBotHelp@gmail.com>
|
||||
Jean-Louis Férey <jeanlouis.ferey@orange.com>
|
||||
jerard@alfa-safety.fr <jrrdev@users.noreply.github.com>
|
||||
Jess <jessachandler@gmail.com>
|
||||
Jonathan Lestrelin <jonathan.lestrelin@gmail.com>
|
||||
Julien Dessaux <julien.dessaux@adyxax.org>
|
||||
Konstantin Bakaras <k.bakaras@voskhod.ru>
|
||||
Manfred Touron <94029+moul@users.noreply.github.com>
|
||||
Manfred Touron <m@42.am>
|
||||
Manuel <manuel.sabban@nbs-system.com>
|
||||
Manuel Sabban <manu@sabban.eu>
|
||||
Manuel Sabban <msa@nbs-system.com>
|
||||
Mathieu Pasquet <mathieu.pasquet@alterway.fr>
|
||||
Mikael Rapp <micke.rapp@gmail.com>
|
||||
MitaliBo <mitali.bisht14@gmail.com>
|
||||
moul-bot <bot@moul.io>
|
||||
Nelly Asher <karmelylle@rambler.ru>
|
||||
NocFlame <aad@nocflame.se>
|
||||
Quentin Perez <qperez42@gmail.com>
|
||||
Renovate Bot <bot@renovateapp.com>
|
||||
Sergey Yashchuk <sergey.yashchuk@coins.ph>
|
||||
Shawn Wang <shawn111@gmail.com>
|
||||
Valentin Daviot <valentin.daviot@alterway.fr>
|
||||
valentin.daviot <valentin.daviot@alterway.fr>
|
||||
welderpb <welderpb@users.noreply.github.com>
|
||||
Дмитрий Шульгачик <tech@uniplug.ru>
|
||||
28
CHANGELOG.md
@@ -1,29 +1,3 @@
|
||||
# Changelog
|
||||
|
||||
## v1.2.0 (2017-11-22)
|
||||
|
||||
* Support adding multiple `--group` links on `host create` and `user create`
|
||||
* Use govalidator to perform more consistent input validation
|
||||
* Use a database migration system
|
||||
|
||||
## v1.1.0 (2017-11-15)
|
||||
|
||||
* Improve versionning (static VERSION + dynamic GIT_* info)
|
||||
* Configuration management (backup + restore)
|
||||
* Implement Exit (fix [#6](https://github.com/moul/sshportal/pull/6))
|
||||
* Disable mysql support (not fully working right now)
|
||||
* Set random seed properly
|
||||
|
||||
## v1.0.0 (2017-11-14)
|
||||
|
||||
Initial version
|
||||
|
||||
* Host management
|
||||
* User management
|
||||
* User Group management
|
||||
* Host Group management
|
||||
* Host Key management
|
||||
* User Key management
|
||||
* ACL management
|
||||
* Connect to host using key or password
|
||||
* Admin commands can be run directly or in an interactive shell
|
||||
Here: https://github.com/moul/sshportal/releases
|
||||
|
||||
14
Dockerfile
@@ -1,10 +1,16 @@
|
||||
# build
|
||||
FROM golang:1.9 as builder
|
||||
COPY . /go/src/github.com/moul/sshportal
|
||||
WORKDIR /go/src/github.com/moul/sshportal
|
||||
FROM golang:1.16.2 as builder
|
||||
ENV GO111MODULE=on
|
||||
WORKDIR /go/src/moul.io/sshportal
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . ./
|
||||
RUN make _docker_install
|
||||
|
||||
# minimal runtime
|
||||
FROM scratch
|
||||
FROM alpine
|
||||
COPY --from=builder /go/bin/sshportal /bin/sshportal
|
||||
ENTRYPOINT ["/bin/sshportal"]
|
||||
CMD ["server"]
|
||||
EXPOSE 2222
|
||||
HEALTHCHECK CMD /bin/sshportal healthcheck --wait
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
@@ -176,13 +175,24 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2013-2017 Docker, Inc.
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017-2021 Manfred Touron <m@42.am>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
53
Makefile
@@ -1,36 +1,47 @@
|
||||
GIT_SHA ?= $(shell git rev-parse HEAD)
|
||||
GIT_TAG ?= $(shell git describe --tags --always)
|
||||
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
LDFLAGS ?= -X main.GIT_SHA=$(GIT_SHA) -X main.GIT_TAG=$(GIT_TAG) -X main.GIT_BRANCH=$(GIT_BRANCH)
|
||||
VERSION ?= $(shell grep 'VERSION =' main.go | cut -d'"' -f2)
|
||||
GOPKG ?= moul.io/sshportal
|
||||
GOBINS ?= .
|
||||
DOCKER_IMAGE ?= moul/sshportal
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
go install -ldflags '$(LDFLAGS)' .
|
||||
VERSION ?= `git describe --tags --always`
|
||||
VCS_REF ?= `git rev-parse --short HEAD`
|
||||
GO_INSTALL_OPTS = -ldflags="-X main.GitSha=$(VCS_REF) -X main.GitTag=$(VERSION)"
|
||||
PORT ?= 2222
|
||||
|
||||
.PHONY: docker.build
|
||||
docker.build:
|
||||
docker build -t moul/sshportal .
|
||||
include rules.mk
|
||||
|
||||
DB_VERSION ?= v$(shell grep -E 'ID: "[0-9]+",' pkg/bastion/dbinit.go | tail -n 1 | cut -d'"' -f2)
|
||||
AES_KEY ?= my-dummy-aes-key
|
||||
|
||||
.PHONY: integration
|
||||
integration:
|
||||
bash ./examples/integration/test.sh
|
||||
cd ./examples/integration && make
|
||||
|
||||
.PHONY: _docker_install
|
||||
_docker_install:
|
||||
CGO_ENABLED=1 go build -ldflags '-extldflags "-static" $(LDFLAGS)' -tags netgo -v -o /go/bin/sshportal
|
||||
CGO_ENABLED=1 $(GO) build -ldflags '-extldflags "-static" $(LDFLAGS)' -tags netgo -v -o /go/bin/sshportal
|
||||
|
||||
.PHONY: dev
|
||||
dev:
|
||||
-go get github.com/githubnemo/CompileDaemon
|
||||
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal --demo --debug" .
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -i .
|
||||
go test -v .
|
||||
-$(GO) get github.com/githubnemo/CompileDaemon
|
||||
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal server --debug --bind-address=:$(PORT) --aes-key=$(AES_KEY) $(EXTRA_RUN_OPTS)" .
|
||||
|
||||
.PHONY: backup
|
||||
backup:
|
||||
mkdir -p data/backups
|
||||
cp sshportal.db data/backups/$(shell date +%s)-$(VERSION)-sshportal.sqlite
|
||||
cp sshportal.db data/backups/$(shell date +%s)-$(DB_VERSION)-sshportal.sqlite
|
||||
|
||||
doc:
|
||||
dot -Tsvg ./.assets/overview.dot > ./.assets/overview.svg
|
||||
dot -Tsvg ./.assets/cluster-mysql.dot > ./.assets/cluster-mysql.svg
|
||||
dot -Tsvg ./.assets/flow-diagram.dot > ./.assets/flow-diagram.svg
|
||||
dot -Tpng ./.assets/overview.dot > ./.assets/overview.png
|
||||
dot -Tpng ./.assets/cluster-mysql.dot > ./.assets/cluster-mysql.png
|
||||
dot -Tpng ./.assets/flow-diagram.dot > ./.assets/flow-diagram.png
|
||||
|
||||
.PHONY: goreleaser
|
||||
goreleaser:
|
||||
GORELEASER_GITHUB_TOKEN=$(GORELEASER_GITHUB_TOKEN) GITHUB_TOKEN=$(GITHUB_TOKEN) goreleaser --rm-dist
|
||||
|
||||
.PHONY: goreleaser-dry-run
|
||||
goreleaser-dry-run:
|
||||
goreleaser --snapshot --skip-publish --rm-dist
|
||||
|
||||
457
README.md
@@ -1,43 +1,50 @@
|
||||
# sshportal
|
||||
|
||||
[](https://circleci.com/gh/moul/sshportal)
|
||||
[](https://goreportcard.com/report/moul.io/sshportal)
|
||||
[](https://godoc.org/moul.io/sshportal)
|
||||
[](https://opencollective.com/sshportal) [](https://github.com/moul/sshportal/blob/master/LICENSE)
|
||||
[](https://github.com/moul/sshportal/releases)
|
||||
<!-- temporarily broken? [](https://hub.docker.com/r/moul/sshportal/) -->
|
||||
|
||||
Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
|
||||
|
||||
```
|
||||
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
DMZ │
|
||||
┌────────┐ │ ┌────────┐
|
||||
│ homer │───▶╔═════════════════╗───▶│ host1 │ │
|
||||
└────────┘ ║ ║ └────────┘
|
||||
┌────────┐ ║ ║ ┌────────┐ │
|
||||
│ bart │───▶║ sshportal ║───▶│ host2 │
|
||||
└────────┘ ║ ║ └────────┘ │
|
||||
┌────────┐ ║ ║ ┌────────┐
|
||||
│ lisa │───▶╚═════════════════╝───▶│ host3 │ │
|
||||
└────────┘ │ └────────┘
|
||||
┌────────┐ ┌────────┐ │
|
||||
│ ... │ │ │ ... │
|
||||
└────────┘ └────────┘ │
|
||||
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
```
|
||||
<img src="https://raw.githubusercontent.com/moul/sshportal/master/.assets/bastion.jpg" width="50%">
|
||||
|
||||
## Features
|
||||
Features include: independence of users and hosts, convenient user invite system, connecting to servers that don't support SSH keys, various levels of access, and many more. Easy to install, run and configure.
|
||||
|
||||
* Host management
|
||||
* User management
|
||||
* User Group management
|
||||
* Host Group management
|
||||
* Host Key management
|
||||
* User Key management
|
||||
* ACL management
|
||||
* Connect to host using key or password
|
||||
* Admin commands can be run directly or in an interactive shell
|
||||

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

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

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

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

|
||||
|
||||
## Contributors
|
||||
|
||||
### Code Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
|
||||
<a href="https://github.com/moul/sshportal/graphs/contributors"><img src="https://opencollective.com/sshportal/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
### Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/sshportal/contribute)]
|
||||
|
||||
#### Individuals
|
||||
|
||||
<a href="https://opencollective.com/sshportal"><img src="https://opencollective.com/sshportal/individuals.svg?width=890"></a>
|
||||
|
||||
#### Organizations
|
||||
|
||||
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/sshportal/contribute)]
|
||||
|
||||
<a href="https://opencollective.com/sshportal/organization/0/website"><img src="https://opencollective.com/sshportal/organization/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/1/website"><img src="https://opencollective.com/sshportal/organization/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/2/website"><img src="https://opencollective.com/sshportal/organization/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/3/website"><img src="https://opencollective.com/sshportal/organization/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/4/website"><img src="https://opencollective.com/sshportal/organization/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/5/website"><img src="https://opencollective.com/sshportal/organization/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/6/website"><img src="https://opencollective.com/sshportal/organization/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/7/website"><img src="https://opencollective.com/sshportal/organization/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/8/website"><img src="https://opencollective.com/sshportal/organization/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/sshportal/organization/9/website"><img src="https://opencollective.com/sshportal/organization/9/avatar.svg"></a>
|
||||
|
||||
### Stargazers over time
|
||||
|
||||
[](https://starchart.cc/moul/sshportal)
|
||||
|
||||
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-slate
|
||||
40
acl.go
@@ -1,40 +0,0 @@
|
||||
package main
|
||||
|
||||
import "sort"
|
||||
|
||||
type ByWeight []*ACL
|
||||
|
||||
func (a ByWeight) Len() int { return len(a) }
|
||||
func (a ByWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByWeight) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
|
||||
|
||||
func CheckACLs(user User, host Host) (string, error) {
|
||||
// shared ACLs between user and host
|
||||
aclMap := map[uint]*ACL{}
|
||||
for _, userGroup := range user.Groups {
|
||||
for _, userGroupACL := range userGroup.ACLs {
|
||||
for _, hostGroup := range host.Groups {
|
||||
for _, hostGroupACL := range hostGroup.ACLs {
|
||||
if userGroupACL.ID == hostGroupACL.ID {
|
||||
aclMap[userGroupACL.ID] = userGroupACL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// FIXME: add ACLs that match host pattern
|
||||
|
||||
// deny by default if no shared ACL
|
||||
if len(aclMap) == 0 {
|
||||
return "deny", nil // default action
|
||||
}
|
||||
|
||||
// transofrm map to slice and sort it
|
||||
acls := []*ACL{}
|
||||
for _, acl := range aclMap {
|
||||
acls = append(acls, acl)
|
||||
}
|
||||
sort.Sort(ByWeight(acls))
|
||||
|
||||
return acls[0].Action, nil
|
||||
}
|
||||
43
acl_test.go
@@ -1,43 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestCheckACLs(t *testing.T) {
|
||||
Convey("Testing CheckACLs", t, func() {
|
||||
// create tmp dir
|
||||
tempDir, err := ioutil.TempDir("", "sshportal")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// create sqlite db
|
||||
db, err := gorm.Open("sqlite3", filepath.Join(tempDir, "sshportal.db"))
|
||||
db.LogMode(false)
|
||||
So(dbInit(db), ShouldBeNil)
|
||||
|
||||
// create dummy objects
|
||||
hostGroup, err := FindHostGroupByIdOrName(db, "default")
|
||||
So(err, ShouldBeNil)
|
||||
db.Create(&Host{Groups: []HostGroup{*hostGroup}})
|
||||
|
||||
//. load db
|
||||
var (
|
||||
hosts []Host
|
||||
users []User
|
||||
)
|
||||
db.Preload("Groups").Preload("Groups.ACLs").Find(&hosts)
|
||||
db.Preload("Groups").Preload("Groups.ACLs").Find(&users)
|
||||
|
||||
// test
|
||||
action, err := CheckACLs(users[0], hosts[0])
|
||||
So(err, ShouldBeNil)
|
||||
So(action, ShouldEqual, "allow")
|
||||
})
|
||||
}
|
||||
49
crypto.go
@@ -1,49 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func NewSSHKey(keyType string, length uint) (*SSHKey, error) {
|
||||
key := SSHKey{
|
||||
Type: keyType,
|
||||
Length: length,
|
||||
}
|
||||
|
||||
// generate the private key
|
||||
if keyType != "rsa" {
|
||||
return nil, fmt.Errorf("key type not supported: %q", key.Type)
|
||||
}
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// convert priv key to x509 format
|
||||
var pemKey = &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||
}
|
||||
buf := bytes.NewBufferString("")
|
||||
if err = pem.Encode(buf, pemKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key.PrivKey = buf.String()
|
||||
|
||||
// generte authorized-key formatted pubkey output
|
||||
pub, err := gossh.NewPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key.PubKey = strings.TrimSpace(string(gossh.MarshalAuthorizedKey(pub)))
|
||||
|
||||
return &key, nil
|
||||
}
|
||||
302
db.go
@@ -1,302 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
SSHKeys []*SSHKey `json:"keys"`
|
||||
Hosts []*Host `json:"hosts"`
|
||||
UserKeys []*UserKey `json:"user_keys"`
|
||||
Users []*User `json:"users"`
|
||||
UserGroups []*UserGroup `json:"user_groups"`
|
||||
HostGroups []*HostGroup `json:"host_groups"`
|
||||
ACLs []*ACL `json:"acls"`
|
||||
Settings []*Setting `json:"settings"`
|
||||
Date time.Time `json:"date"`
|
||||
}
|
||||
|
||||
type Setting struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required"`
|
||||
Value string `valid:"required"`
|
||||
}
|
||||
|
||||
type SSHKey struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Type string `valid:"required"`
|
||||
Length uint `valid:"required"`
|
||||
Fingerprint string `valid:"optional"`
|
||||
PrivKey string `sql:"size:10000" valid:"required"`
|
||||
PubKey string `sql:"size:10000" valid:"optional"`
|
||||
Hosts []*Host `gorm:"ForeignKey:SSHKeyID"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type Host struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
|
||||
Addr string `valid:"required"`
|
||||
User string `valid:"optional"`
|
||||
Password string `valid:"optional"`
|
||||
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Fingerprint string `valid:"optional"` // FIXME: replace with hostKey ?
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type UserKey struct {
|
||||
gorm.Model
|
||||
Key []byte `sql:"size:10000" valid:"required,length(1|10000)"`
|
||||
UserID uint ``
|
||||
User *User `gorm:"ForeignKey:UserID"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
IsAdmin bool
|
||||
Email string `valid:"required,email"`
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Keys []*UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string `valid:"optional"`
|
||||
InviteToken string `valid:"optional,length(10|60)"`
|
||||
}
|
||||
|
||||
type UserGroup struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Users []*User `gorm:"many2many:user_user_groups;"`
|
||||
ACLs []*ACL `gorm:"many2many:user_group_acls;"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type HostGroup struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Hosts []*Host `gorm:"many2many:host_host_groups;"`
|
||||
ACLs []*ACL `gorm:"many2many:host_group_acls;"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type ACL struct {
|
||||
gorm.Model
|
||||
HostGroups []*HostGroup `gorm:"many2many:host_group_acls;"`
|
||||
UserGroups []*UserGroup `gorm:"many2many:user_group_acls;"`
|
||||
HostPattern string `valid:"optional"`
|
||||
Action string `valid:"required"`
|
||||
Weight uint ``
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*")
|
||||
|
||||
govalidator.CustomTypeTagMap.Set("unix_user", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||
name, ok := i.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return unixUserRegexp.MatchString(name)
|
||||
}))
|
||||
}
|
||||
|
||||
func RemoteHostFromSession(s ssh.Session, db *gorm.DB) (*Host, error) {
|
||||
var host Host
|
||||
db.Preload("SSHKey").Where("name = ?", s.User()).Find(&host)
|
||||
if host.Name == "" {
|
||||
// FIXME: add available hosts
|
||||
return nil, fmt.Errorf("No such target: %q", s.User())
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
func (host *Host) URL() string {
|
||||
return fmt.Sprintf("%s@%s", host.User, host.Addr)
|
||||
}
|
||||
|
||||
func NewHostFromURL(rawurl string) (*Host, error) {
|
||||
if !strings.Contains(rawurl, "://") {
|
||||
rawurl = "ssh://" + rawurl
|
||||
}
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host := Host{Addr: u.Host}
|
||||
if !strings.Contains(host.Addr, ":") {
|
||||
host.Addr += ":22" // add port if not present
|
||||
}
|
||||
host.User = "root" // default username
|
||||
if u.User != nil {
|
||||
password, _ := u.User.Password()
|
||||
host.Password = password
|
||||
host.User = u.User.Username()
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
func (host *Host) Hostname() string {
|
||||
return strings.Split(host.Addr, ":")[0]
|
||||
}
|
||||
|
||||
// Host helpers
|
||||
|
||||
func FindHostByIdOrName(db *gorm.DB, query string) (*Host, error) {
|
||||
var host Host
|
||||
if err := db.Preload("Groups").Preload("SSHKey").Where("id = ?", query).Or("name = ?", query).First(&host).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
func FindHostsByIdOrName(db *gorm.DB, queries []string) ([]*Host, error) {
|
||||
var hosts []*Host
|
||||
for _, query := range queries {
|
||||
host, err := FindHostByIdOrName(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
// SSHKey helpers
|
||||
|
||||
func FindKeyByIdOrName(db *gorm.DB, query string) (*SSHKey, error) {
|
||||
var key SSHKey
|
||||
if err := db.Preload("Hosts").Where("id = ?", query).Or("name = ?", query).First(&key).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &key, nil
|
||||
}
|
||||
func FindKeysByIdOrName(db *gorm.DB, queries []string) ([]*SSHKey, error) {
|
||||
var keys []*SSHKey
|
||||
for _, query := range queries {
|
||||
key, err := FindKeyByIdOrName(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// HostGroup helpers
|
||||
|
||||
func FindHostGroupByIdOrName(db *gorm.DB, query string) (*HostGroup, error) {
|
||||
var hostGroup HostGroup
|
||||
if err := db.Preload("ACLs").Preload("Hosts").Where("id = ?", query).Or("name = ?", query).First(&hostGroup).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &hostGroup, nil
|
||||
}
|
||||
func FindHostGroupsByIdOrName(db *gorm.DB, queries []string) ([]*HostGroup, error) {
|
||||
var hostGroups []*HostGroup
|
||||
for _, query := range queries {
|
||||
hostGroup, err := FindHostGroupByIdOrName(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hostGroups = append(hostGroups, hostGroup)
|
||||
}
|
||||
return hostGroups, nil
|
||||
}
|
||||
|
||||
// UserGroup heleprs
|
||||
|
||||
func FindUserGroupByIdOrName(db *gorm.DB, query string) (*UserGroup, error) {
|
||||
var userGroup UserGroup
|
||||
if err := db.Preload("ACLs").Preload("Users").Where("id = ?", query).Or("name = ?", query).First(&userGroup).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &userGroup, nil
|
||||
}
|
||||
func FindUserGroupsByIdOrName(db *gorm.DB, queries []string) ([]*UserGroup, error) {
|
||||
var userGroups []*UserGroup
|
||||
for _, query := range queries {
|
||||
userGroup, err := FindUserGroupByIdOrName(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userGroups = append(userGroups, userGroup)
|
||||
}
|
||||
return userGroups, nil
|
||||
}
|
||||
|
||||
// User helpers
|
||||
|
||||
func FindUserByIdOrEmail(db *gorm.DB, query string) (*User, error) {
|
||||
var user User
|
||||
if err := db.Preload("Groups").Preload("Keys").Where("id = ?", query).Or("email = ?", query).First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
func FindUsersByIdOrEmail(db *gorm.DB, queries []string) ([]*User, error) {
|
||||
var users []*User
|
||||
for _, query := range queries {
|
||||
user, err := FindUserByIdOrEmail(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// ACL helpers
|
||||
|
||||
func FindACLById(db *gorm.DB, query string) (*ACL, error) {
|
||||
var acl ACL
|
||||
if err := db.Preload("UserGroups").Preload("HostGroups").Where("id = ?", query).First(&acl).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &acl, nil
|
||||
}
|
||||
func FindACLsById(db *gorm.DB, queries []string) ([]*ACL, error) {
|
||||
var acls []*ACL
|
||||
for _, query := range queries {
|
||||
acl, err := FindACLById(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acls = append(acls, acl)
|
||||
}
|
||||
return acls, nil
|
||||
}
|
||||
|
||||
// UserKey helpers
|
||||
|
||||
func FindUserkeyById(db *gorm.DB, query string) (*UserKey, error) {
|
||||
var userkey UserKey
|
||||
if err := db.Preload("User").Where("id = ?", query).First(&userkey).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &userkey, nil
|
||||
}
|
||||
func FindUserkeysById(db *gorm.DB, queries []string) ([]*UserKey, error) {
|
||||
var userkeys []*UserKey
|
||||
for _, query := range queries {
|
||||
userkey, err := FindUserkeyById(db, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userkeys = append(userkeys, userkey)
|
||||
}
|
||||
return userkeys, nil
|
||||
}
|
||||
339
dbinit.go
@@ -1,339 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
func dbInit(db *gorm.DB) error {
|
||||
m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
|
||||
{
|
||||
ID: "1",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Setting struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
return tx.AutoMigrate(&Setting{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("settings").Error
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type SSHKey struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string
|
||||
Type string
|
||||
Length uint
|
||||
Fingerprint string
|
||||
PrivKey string `sql:"size:10000"`
|
||||
PubKey string `sql:"size:10000"`
|
||||
Hosts []*Host `gorm:"ForeignKey:SSHKeyID"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&SSHKey{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("ssh_keys").Error
|
||||
},
|
||||
}, {
|
||||
ID: "3",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32"`
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Fingerprint string
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("hosts").Error
|
||||
},
|
||||
}, {
|
||||
ID: "4",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type UserKey struct {
|
||||
gorm.Model
|
||||
Key []byte `sql:"size:10000"`
|
||||
UserID uint ``
|
||||
User *User `gorm:"ForeignKey:UserID"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&UserKey{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("user_keys").Error
|
||||
},
|
||||
}, {
|
||||
ID: "5",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type User struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
IsAdmin bool
|
||||
Email string
|
||||
Name string
|
||||
Keys []*UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string
|
||||
InviteToken string
|
||||
}
|
||||
return tx.AutoMigrate(&User{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("users").Error
|
||||
},
|
||||
}, {
|
||||
ID: "6",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type UserGroup struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Users []*User `gorm:"many2many:user_user_groups;"`
|
||||
ACLs []*ACL `gorm:"many2many:user_group_acls;"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&UserGroup{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("user_groups").Error
|
||||
},
|
||||
}, {
|
||||
ID: "7",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type HostGroup struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Hosts []*Host `gorm:"many2many:host_host_groups;"`
|
||||
ACLs []*ACL `gorm:"many2many:host_group_acls;"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&HostGroup{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("host_groups").Error
|
||||
},
|
||||
}, {
|
||||
ID: "8",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type ACL struct {
|
||||
gorm.Model
|
||||
HostGroups []*HostGroup `gorm:"many2many:host_group_acls;"`
|
||||
UserGroups []*UserGroup `gorm:"many2many:user_group_acls;"`
|
||||
HostPattern string
|
||||
Action string
|
||||
Weight uint
|
||||
Comment string
|
||||
}
|
||||
|
||||
return tx.AutoMigrate(&ACL{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("acls").Error
|
||||
},
|
||||
}, {
|
||||
ID: "9",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&Setting{}).RemoveIndex("uix_settings_name")
|
||||
return db.Model(&Setting{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_settings_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&Setting{}).RemoveIndex("uix_settings_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "10",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&SSHKey{}).RemoveIndex("uix_keys_name")
|
||||
return db.Model(&SSHKey{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_keys_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&SSHKey{}).RemoveIndex("uix_keys_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "11",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&Host{}).RemoveIndex("uix_hosts_name")
|
||||
return db.Model(&Host{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_hosts_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&Host{}).RemoveIndex("uix_hosts_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "12",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&User{}).RemoveIndex("uix_users_name")
|
||||
return db.Model(&User{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_users_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&User{}).RemoveIndex("uix_users_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "13",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&UserGroup{}).RemoveIndex("uix_usergroups_name")
|
||||
return db.Model(&UserGroup{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_usergroups_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&UserGroup{}).RemoveIndex("uix_usergroups_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "14",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&HostGroup{}).RemoveIndex("uix_hostgroups_name")
|
||||
return db.Model(&HostGroup{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_hostgroups_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&HostGroup{}).RemoveIndex("uix_hostgroups_name").Error
|
||||
},
|
||||
},
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create default ssh key
|
||||
var count uint
|
||||
if err := db.Table("ssh_keys").Where("name = ?", "default").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
key, err := NewSSHKey("rsa", 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.Name = "default"
|
||||
key.Comment = "created by sshportal"
|
||||
if err := db.Create(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create default host group
|
||||
if err := db.Table("host_groups").Where("name = ?", "default").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
hostGroup := HostGroup{
|
||||
Name: "default",
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
if err := db.Create(&hostGroup).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create default user group
|
||||
if err := db.Table("user_groups").Where("name = ?", "default").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
userGroup := UserGroup{
|
||||
Name: "default",
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
if err := db.Create(&userGroup).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create default acl
|
||||
if err := db.Table("acls").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
var defaultUserGroup UserGroup
|
||||
db.Where("name = ?", "default").First(&defaultUserGroup)
|
||||
var defaultHostGroup HostGroup
|
||||
db.Where("name = ?", "default").First(&defaultHostGroup)
|
||||
acl := ACL{
|
||||
UserGroups: []*UserGroup{&defaultUserGroup},
|
||||
HostGroups: []*HostGroup{&defaultHostGroup},
|
||||
Action: "allow",
|
||||
//HostPattern: "",
|
||||
//Weight: 0,
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
if err := db.Create(&acl).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create admin user
|
||||
var defaultUserGroup UserGroup
|
||||
db.Where("name = ?", "default").First(&defaultUserGroup)
|
||||
db.Table("users").Count(&count)
|
||||
if count == 0 {
|
||||
// if no admin, create an account for the first connection
|
||||
inviteToken := RandStringBytes(16)
|
||||
if os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN") != "" {
|
||||
inviteToken = os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN")
|
||||
}
|
||||
user := User{
|
||||
Name: "Administrator",
|
||||
Email: "admin@sshportal",
|
||||
Comment: "created by sshportal",
|
||||
IsAdmin: true,
|
||||
InviteToken: inviteToken,
|
||||
Groups: []*UserGroup{&defaultUserGroup},
|
||||
}
|
||||
db.Create(&user)
|
||||
log.Printf("Admin user created, use the user 'invite:%s' to associate a public key with this account", user.InviteToken)
|
||||
}
|
||||
|
||||
// create host ssh key
|
||||
if err := db.Table("ssh_keys").Where("name = ?", "host").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
key, err := NewSSHKey("rsa", 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.Name = "host"
|
||||
key.Comment = "created by sshportal"
|
||||
if err := db.Create(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dbDemo(db *gorm.DB) error {
|
||||
hostGroup, err := FindHostGroupByIdOrName(db, "default")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := FindKeyByIdOrName(db, "default")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
host1 = Host{Name: "sdf", Addr: "sdf.org:22", User: "new", SSHKeyID: key.ID, Groups: []*HostGroup{hostGroup}}
|
||||
host2 = Host{Name: "whoami", Addr: "whoami.filippo.io:22", User: "test", SSHKeyID: key.ID, Groups: []*HostGroup{hostGroup}}
|
||||
host3 = Host{Name: "ssh-chat", Addr: "chat.shazow.net:22", User: "test", SSHKeyID: key.ID, Fingerprint: "MD5:e5:d5:d1:75:90:38:42:f6:c7:03:d7:d0:56:7d:6a:db", Groups: []*HostGroup{hostGroup}}
|
||||
)
|
||||
|
||||
// FIXME: check if hosts exist to avoid `UNIQUE constraint` error
|
||||
db.FirstOrCreate(&host1)
|
||||
db.FirstOrCreate(&host2)
|
||||
db.FirstOrCreate(&host3)
|
||||
return nil
|
||||
}
|
||||
127
depaware.txt
Normal file
@@ -0,0 +1,127 @@
|
||||
moul.io/sshportal dependencies: (generated by github.com/tailscale/depaware)
|
||||
|
||||
github.com/anmitsu/go-shlex from github.com/gliderlabs/ssh+
|
||||
github.com/asaskevich/govalidator from moul.io/sshportal/pkg/bastion+
|
||||
github.com/cpuguy83/go-md2man/v2/md2man from github.com/urfave/cli
|
||||
LD 💣 github.com/creack/pty from github.com/kr/pty
|
||||
github.com/docker/docker/pkg/namesgenerator from moul.io/sshportal/pkg/bastion
|
||||
github.com/docker/docker/pkg/random from github.com/docker/docker/pkg/namesgenerator
|
||||
github.com/dustin/go-humanize from moul.io/sshportal/pkg/bastion
|
||||
github.com/gliderlabs/ssh from moul.io/sshportal+
|
||||
github.com/go-sql-driver/mysql from github.com/jinzhu/gorm/dialects/mysql+
|
||||
github.com/jinzhu/gorm from gopkg.in/gormigrate.v1+
|
||||
github.com/jinzhu/gorm/dialects/mysql from moul.io/sshportal
|
||||
github.com/jinzhu/gorm/dialects/postgres from moul.io/sshportal
|
||||
github.com/jinzhu/gorm/dialects/sqlite from moul.io/sshportal
|
||||
github.com/jinzhu/inflection from github.com/jinzhu/gorm
|
||||
LD github.com/kr/pty from moul.io/sshportal
|
||||
github.com/lib/pq from github.com/jinzhu/gorm/dialects/postgres
|
||||
github.com/lib/pq/hstore from github.com/jinzhu/gorm/dialects/postgres
|
||||
github.com/lib/pq/oid from github.com/lib/pq
|
||||
github.com/lib/pq/scram from github.com/lib/pq
|
||||
💣 github.com/mattn/go-colorable from github.com/mgutz/ansi
|
||||
💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable
|
||||
github.com/mattn/go-runewidth from github.com/olekukonko/tablewriter
|
||||
💣 github.com/mattn/go-sqlite3 from github.com/jinzhu/gorm/dialects/sqlite
|
||||
github.com/mgutz/ansi from moul.io/sshportal/pkg/bastion
|
||||
github.com/olekukonko/tablewriter from moul.io/sshportal/pkg/bastion
|
||||
github.com/pkg/errors from moul.io/sshportal/pkg/bastion
|
||||
github.com/reiver/go-oi from github.com/reiver/go-telnet+
|
||||
github.com/reiver/go-telnet from moul.io/sshportal/pkg/bastion
|
||||
github.com/russross/blackfriday/v2 from github.com/cpuguy83/go-md2man/v2/md2man
|
||||
github.com/sabban/bastion/pkg/logchannel from moul.io/sshportal/pkg/bastion
|
||||
github.com/shurcooL/sanitized_anchor_name from github.com/russross/blackfriday/v2
|
||||
github.com/urfave/cli from moul.io/sshportal+
|
||||
gopkg.in/gormigrate.v1 from moul.io/sshportal/pkg/bastion
|
||||
moul.io/srand from moul.io/sshportal
|
||||
moul.io/sshportal/pkg/bastion from moul.io/sshportal
|
||||
moul.io/sshportal/pkg/crypto from moul.io/sshportal/pkg/bastion
|
||||
moul.io/sshportal/pkg/dbmodels from moul.io/sshportal/pkg/bastion+
|
||||
golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/ssh from github.com/gliderlabs/ssh+
|
||||
golang.org/x/crypto/ssh/terminal from moul.io/sshportal/pkg/bastion
|
||||
golang.org/x/net/dns/dnsmessage from net
|
||||
D golang.org/x/net/route from net
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/chacha20poly1305
|
||||
LD golang.org/x/sys/unix from github.com/mattn/go-isatty+
|
||||
W golang.org/x/sys/windows from golang.org/x/crypto/ssh/terminal
|
||||
bufio from crypto/rand+
|
||||
bytes from bufio+
|
||||
container/list from crypto/tls
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
crypto/aes from crypto/ecdsa+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
crypto/dsa from crypto/x509+
|
||||
crypto/ecdsa from crypto/tls+
|
||||
crypto/ed25519 from crypto/tls+
|
||||
crypto/elliptic from crypto/ecdsa+
|
||||
crypto/hmac from crypto/tls+
|
||||
crypto/md5 from crypto/tls+
|
||||
crypto/rand from crypto/ed25519+
|
||||
crypto/rc4 from crypto/tls+
|
||||
crypto/rsa from crypto/tls+
|
||||
crypto/sha1 from crypto/tls+
|
||||
crypto/sha256 from crypto/tls+
|
||||
crypto/sha512 from crypto/ecdsa+
|
||||
crypto/subtle from crypto/aes+
|
||||
crypto/tls from github.com/go-sql-driver/mysql+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509
|
||||
database/sql from github.com/go-sql-driver/mysql+
|
||||
database/sql/driver from database/sql+
|
||||
encoding from encoding/json
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from crypto/aes+
|
||||
encoding/csv from github.com/olekukonko/tablewriter
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from github.com/asaskevich/govalidator+
|
||||
encoding/pem from crypto/tls+
|
||||
errors from bufio+
|
||||
flag from github.com/urfave/cli
|
||||
fmt from crypto/tls+
|
||||
go/ast from github.com/jinzhu/gorm
|
||||
go/scanner from go/ast
|
||||
go/token from go/ast+
|
||||
hash from crypto+
|
||||
html from github.com/asaskevich/govalidator+
|
||||
io from bufio+
|
||||
io/ioutil from crypto/tls+
|
||||
log from github.com/gliderlabs/ssh+
|
||||
math from crypto/rsa+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from crypto/md5+
|
||||
math/rand from github.com/docker/docker/pkg/random+
|
||||
net from crypto/tls+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
LD os/exec from github.com/creack/pty+
|
||||
os/user from github.com/lib/pq+
|
||||
path from github.com/asaskevich/govalidator+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/asaskevich/govalidator+
|
||||
regexp/syntax from regexp
|
||||
sort from database/sql+
|
||||
strconv from crypto+
|
||||
strings from bufio+
|
||||
sync from context+
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from github.com/urfave/cli
|
||||
text/template from github.com/urfave/cli
|
||||
text/template/parse from text/template
|
||||
time from context+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
unicode/utf8 from bufio+
|
||||
24
examples/homebrew/sshportal.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
require "language/go"
|
||||
|
||||
class Sshportal < Formula
|
||||
desc "sshportal: simple, fun and transparent SSH bastion"
|
||||
homepage "https://github.com/moul/sshportal"
|
||||
url "https://github.com/moul/sshportal/archive/v1.7.1.tar.gz"
|
||||
sha256 "4611ae2f30cc595b2fb789bd0c92550533db6d4b63c638dd78cf85517b6aeaf0"
|
||||
head "https://github.com/moul/sshportal.git"
|
||||
|
||||
depends_on "go" => :build
|
||||
|
||||
def install
|
||||
ENV["GOPATH"] = buildpath
|
||||
ENV["GOBIN"] = buildpath
|
||||
(buildpath/"src/github.com/moul/sshportal").install Dir["*"]
|
||||
|
||||
system "go", "build", "-o", "#{bin}/sshportal", "-v", "github.com/moul/sshportal"
|
||||
end
|
||||
|
||||
test do
|
||||
output = shell_output(bin/"sshportal --version")
|
||||
assert output.include? "sshportal version "
|
||||
end
|
||||
end
|
||||
4
examples/integration/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM occitech/ssh-client
|
||||
ENTRYPOINT ["/bin/sh", "-c"]
|
||||
CMD ["/integration/_client.sh"]
|
||||
COPY . /integration
|
||||
10
examples/integration/Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
run:
|
||||
docker-compose down
|
||||
docker-compose up -d sshportal
|
||||
docker-compose build client
|
||||
docker-compose exec sshportal /bin/sshportal healthcheck --wait --quiet
|
||||
docker-compose run client /integration/_client.sh
|
||||
docker-compose down
|
||||
|
||||
build:
|
||||
docker-compose build
|
||||
79
examples/integration/_client.sh
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
cp /integration/client_test_rsa ~/.ssh/id_rsa
|
||||
chmod -R 700 ~/.ssh
|
||||
cat >~/.ssh/config <<EOF
|
||||
Host sshportal
|
||||
Port 2222
|
||||
HostName sshportal
|
||||
|
||||
Host testserver
|
||||
Port 2222
|
||||
HostName testserver
|
||||
|
||||
Host *
|
||||
StrictHostKeyChecking no
|
||||
ControlMaster auto
|
||||
SendEnv TEST_*
|
||||
|
||||
EOF
|
||||
|
||||
set -x
|
||||
|
||||
# login
|
||||
ssh sshportal -l invite:integration
|
||||
|
||||
# hostgroup/usergroup/acl
|
||||
ssh sshportal -l admin hostgroup create
|
||||
ssh sshportal -l admin hostgroup create --name=hg1
|
||||
ssh sshportal -l admin hostgroup create --name=hg2 --comment=test
|
||||
ssh sshportal -l admin hostgroup inspect hg1 hg2
|
||||
ssh sshportal -l admin hostgroup ls
|
||||
|
||||
ssh sshportal -l admin usergroup create
|
||||
ssh sshportal -l admin usergroup create --name=ug1
|
||||
ssh sshportal -l admin usergroup create --name=ug2 --comment=test
|
||||
ssh sshportal -l admin usergroup inspect ug1 ug2
|
||||
ssh sshportal -l admin usergroup ls
|
||||
|
||||
ssh sshportal -l admin acl create --ug=ug1 --ug=ug2 --hg=hg1 --hg=hg2 --comment=test --action=allow --weight=42
|
||||
ssh sshportal -l admin acl inspect 2
|
||||
ssh sshportal -l admin acl ls
|
||||
|
||||
# basic host create
|
||||
ssh sshportal -l admin host create bob@example.org:1234
|
||||
ssh sshportal -l admin host create test42
|
||||
ssh sshportal -l admin host create --name=testtest --comment=test --password=test test@test.test
|
||||
ssh sshportal -l admin host create --group=hg1 --group=hg2 hostwithgroups.org
|
||||
ssh sshportal -l admin host inspect example test42 testtest hostwithgroups
|
||||
ssh sshportal -l admin host update --assign-group=hg1 test42
|
||||
ssh sshportal -l admin host update --unassign-group=hg1 test42
|
||||
ssh sshportal -l admin host update --assign-group=hg1 test42
|
||||
ssh sshportal -l admin host update --assign-group=hg2 --unassign-group=hg2 test42
|
||||
ssh sshportal -l admin host ls
|
||||
|
||||
# backup/restore
|
||||
ssh sshportal -l admin config backup --indent --ignore-events > backup-1
|
||||
ssh sshportal -l admin config restore --confirm < backup-1
|
||||
ssh sshportal -l admin config backup --indent --ignore-events > backup-2
|
||||
(
|
||||
cat backup-1 | grep -v '"date":' | grep -v 'tedAt":' > backup-1.clean
|
||||
cat backup-2 | grep -v '"date":' | grep -v 'tedAt":' > backup-2.clean
|
||||
set -xe
|
||||
diff backup-1.clean backup-2.clean
|
||||
)
|
||||
|
||||
if [ "$CIRCLECI" = "true" ]; then
|
||||
echo "Strage behavior with cross-container communication on CircleCI, skipping some tests..."
|
||||
else
|
||||
# bastion
|
||||
ssh sshportal -l admin host create --name=testserver toto@testserver:2222
|
||||
out="$(ssh sshportal -l testserver echo hello | head -n 1)"
|
||||
test "$out" = '{"User":"toto","Environ":null,"Command":["echo","hello"]}'
|
||||
|
||||
out="$(TEST_A=1 TEST_B=2 TEST_C=3 TEST_D=4 TEST_E=5 TEST_F=6 TEST_G=7 TEST_H=8 TEST_I=9 ssh sshportal -l testserver echo hello | head -n 1)"
|
||||
test "$out" = '{"User":"toto","Environ":["TEST_A=1","TEST_B=2","TEST_C=3","TEST_D=4","TEST_E=5","TEST_F=6","TEST_G=7","TEST_H=8","TEST_I=9"],"Command":["echo","hello"]}'
|
||||
fi
|
||||
|
||||
# TODO: test more cases (forwards, scp, sftp, interactive, pty, stdin, exit code, ...)
|
||||
27
examples/integration/client_test_rsa
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEAxV0ds/oMuOw9QVLFgxaM0Js2IdJKiYLnmKq96IuZ/wMqMea3
|
||||
qi1UfNBPUQ2CojwbJGTea8cA9J1Et+a6v1mL66YG8zyxmhdlKHm2KOMnUXSfWPNg
|
||||
ZArXH7Uj4Nx1k/O1ujfQFAsYTx63kMqwq1lM9JrExLSdp/8D/zQAyF68c82w8UZH
|
||||
aIpLZJkM/fgh0VJWiw65NYAzuIkJNBgZR8rEBQU7V3lCqFGcSJ88MoqIdVGy0I4b
|
||||
GGpO9VppDTf+uYGYDthhXlV0nHM45neWL5hzFK6oqbLFLpsaUOY7C3kKv+8+B3lX
|
||||
p3OfGVoFy7u3evro+yRQEMQ+myS5UBIHaI3qOwIDAQABAoIBABM7/vASV3kSNOoP
|
||||
2gXrha+y4LStHOyH4HBFe5qVOF3c/hi85ntkTY6YcpJwoaGUAAUs+2w/ib1NMmxF
|
||||
xT9ux68gkB7WdGyTCR3HttQHR0at+fWeSm+Vit+hNKzub1sK7lQGqnW5mxXi5Xrr
|
||||
9gnM+y3/g1u0SoUb2lTdyZG9gdo7LnLElzRinraEqTJUowXkqzAhGf1A+Kgp2fkb
|
||||
/+QP1oiK8QeOFOsITD2UwIVCBRwRl5TjjwfLQ4El6oAWNjcL1ZfSmQLiXZ7U8Smk
|
||||
Cd+BI+6ZDLA43fBUGDjbg4+2dt2JoKNkS0FfqhCW+Z2A0+ClJ8pwuMqRz8XXaOYr
|
||||
ONCqOPECgYEA/qyWxSUjEWMvN3tC/mZPEbwHP3m7mbR1KGwhZylWVCmEF7kVC6il
|
||||
/ICQZUI9ekyGJZ/SKZKwxDe7oeV+vFsus/9FWC5wrp45Xm4kEUwsBr4bWvuNpVOq
|
||||
jrKecY8NgPZS1X6Uc5BbpiE9/VF2gCdYVVCDXP1NfO2MDhkniXJQUEMCgYEAxmQl
|
||||
3s/vih9rXllPZcWHafjnFcGU1AIiJD1c+8lAqwCZzm0Bt0Ex4s1t3lp0ew6YBVXN
|
||||
yGy+BORxOC9FQGTlKZNk/S705+8iAVNc9Sy7XbgN3GY3eat7XYbNpGbQrjiyZ+7I
|
||||
pdEnoHWQD4NFXHaVsXaVHcBFUovXKoes2PODeqkCgYEAoN/3Ucv2zgoAjqSfmkKY
|
||||
mhRT48YLOroi9AjyRM95CCs9lRrGb5n2WH4COOTSHwpuByBhSv+uCBVIwqlNGMDk
|
||||
zLFpZZ3YcoXiqYMb541dlljKwPt8673hVMkCi6uZFSkFBHY0YpgDPPtsxDOMjsHL
|
||||
7ACzKq+cHlmUimdbcViz4S8CgYEAr2+sVYaHixsRtVNA9PxiLQIgR4rx8zEXw/hH
|
||||
m5hyiUV0vaiDlewfEzMab0CKNK/JGx6vZQdUWbsxq7+Re8o9JDDlY0b854T+CzIO
|
||||
x/iQj+XMzBPQBtXvt9sXSsRo0Uft7B6qbIeyhSCxDibFVWjAIzh70N1P8BkdYsyr
|
||||
uwZMRFECgYEA5QuutlFLI7hMPdBQvsEhjdVwKAj7LvpNemgxDpEoMiQWWm51XzcP
|
||||
IZjlCwl1UvIE0MxowtvNr5lQuGRN8/88Dajpq+W6eeTSCKi67nn0VZh13cQLKvoX
|
||||
DRZ6nfC3iLnEYKK+KN/I3NY7JcSjHmW6V8WtrCYAi2D5Ns05XJAG6t8=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
27
examples/integration/docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
version: '3.0'
|
||||
|
||||
services:
|
||||
sshportal:
|
||||
image: moul/sshportal
|
||||
environment:
|
||||
- SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN=integration
|
||||
command: server --debug
|
||||
depends_on:
|
||||
- testserver
|
||||
ports:
|
||||
- 2222
|
||||
|
||||
testserver:
|
||||
image: moul/sshportal
|
||||
command: _test_server
|
||||
ports:
|
||||
- 2222
|
||||
|
||||
client:
|
||||
build: .
|
||||
depends_on:
|
||||
- sshportal
|
||||
- testserver
|
||||
#volumes:
|
||||
# - .:/integration
|
||||
tty: true
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/bin/sh -e
|
||||
# Setup a new sshportal and performs some checks
|
||||
|
||||
PORT=${PORT:-2222}
|
||||
SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN=integration
|
||||
|
||||
# tempdir
|
||||
WORK_DIR=`mktemp -d`
|
||||
if [[ ! "$WORK_DIR" || ! -d "$WORK_DIR" ]]; then
|
||||
echo "Could not create temp dir"
|
||||
exit 1
|
||||
fi
|
||||
cd "${WORK_DIR}"
|
||||
|
||||
# pre cleanup
|
||||
docker_cleanup() {
|
||||
( set -x
|
||||
docker rm -f -v sshportal-integration 2>/dev/null >/dev/null || true
|
||||
)
|
||||
}
|
||||
tempdir_cleanup() {
|
||||
rm -rf "${WORK_DIR}"
|
||||
}
|
||||
docker_cleanup
|
||||
trap tempdir_cleanup EXIT
|
||||
|
||||
# start server
|
||||
( set -xe;
|
||||
docker run \
|
||||
-d \
|
||||
-e SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN=${SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN} \
|
||||
--name=sshportal-integration \
|
||||
-p${PORT}:2222 \
|
||||
moul/sshportal --debug
|
||||
)
|
||||
while ! nc -z localhost ${PORT}; do
|
||||
sleep 1
|
||||
done
|
||||
sleep 3
|
||||
|
||||
# integration suite
|
||||
xssh() {
|
||||
set -e
|
||||
echo "+ ssh {sshportal} $@" >&2
|
||||
ssh -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no localhost -p ${PORT} $@
|
||||
}
|
||||
# login
|
||||
xssh -l invite:integration
|
||||
|
||||
# hostgroup/usergroup/acl
|
||||
xssh -l admin hostgroup create
|
||||
xssh -l admin hostgroup create --name=hg1
|
||||
xssh -l admin hostgroup create --name=hg2 --comment=test
|
||||
xssh -l admin usergroup inspect hg1 hg2
|
||||
xssh -l admin hostgroup ls
|
||||
|
||||
xssh -l admin usergroup create
|
||||
xssh -l admin usergroup create --name=ug1
|
||||
xssh -l admin usergroup create --name=ug2 --comment=test
|
||||
xssh -l admin usergroup inspect ug1 ug2
|
||||
xssh -l admin usergroup ls
|
||||
|
||||
xssh -l admin acl create --ug=ug1 --ug=ug2 --hg=hg1 --hg=hg2 --comment=test --action=allow --weight=42
|
||||
xssh -l admin acl inspect 2
|
||||
xssh -l admin acl ls
|
||||
|
||||
# basic host create
|
||||
xssh -l admin host create bob@example.org:1234
|
||||
xssh -l admin host create test42
|
||||
xssh -l admin host create --name=testtest --comment=test --password=test test@test.test
|
||||
xssh -l admin host create --group=hg1 --group=hg2 hostwithgroups.org
|
||||
xssh -l admin host inspect example test42 testtest hostwithgroups
|
||||
xssh -l admin host ls
|
||||
|
||||
# backup/restore
|
||||
xssh -l admin config backup --indent > backup-1
|
||||
xssh -l admin config restore --confirm < backup-1
|
||||
xssh -l admin config backup --indent > backup-2
|
||||
diff <(cat backup-1 | grep -v '"date":') <(cat backup-2 | grep -v '"date":')
|
||||
|
||||
# post cleanup
|
||||
#cleanup
|
||||
@@ -4,12 +4,16 @@ services:
|
||||
sshportal:
|
||||
build: ../..
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SSHPORTAL_DB_DRIVER: mysql
|
||||
SSHPORTAL_DATABASE_URL: "root:root@tcp(mysql:3306)/db?charset=utf8&parseTime=true&loc=Local"
|
||||
SSHPORTAL_DEBUG: 1
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
links:
|
||||
- mysql
|
||||
command: --db-driver=mysql --debug --db-conn="root:root@tcp(mysql:3306)/db?charset=utf8&parseTime=true&loc=Local"
|
||||
command: server
|
||||
ports:
|
||||
- 2222:2222
|
||||
|
||||
|
||||
35
go.mod
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
module moul.io/sshportal
|
||||
|
||||
require (
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/creack/pty v1.1.11 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/docker v1.13.1
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/gliderlabs/ssh v0.3.2
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||
github.com/jinzhu/gorm v1.9.16
|
||||
github.com/kr/pty v1.1.8
|
||||
github.com/lib/pq v1.8.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/reiver/go-oi v1.0.0
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
|
||||
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4
|
||||
github.com/stretchr/testify v1.4.0 // indirect
|
||||
github.com/urfave/cli v1.22.5
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect
|
||||
gopkg.in/gormigrate.v1 v1.6.0
|
||||
moul.io/srand v1.6.1
|
||||
)
|
||||
|
||||
go 1.14
|
||||
129
go.sum
generated
Normal file
@@ -0,0 +1,129 @@
|
||||
cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
|
||||
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
|
||||
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/gliderlabs/ssh v0.3.2 h1:gcfd1Aj/9RQxvygu4l3sak711f/5+VOwBw9C/7+N4EI=
|
||||
github.com/gliderlabs/ssh v0.3.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
||||
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
|
||||
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
|
||||
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
|
||||
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e h1:quuzZLi72kkJjl+f5AQ93FMcadG19WkS7MO6TXFOSas=
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvEWwEIx86DB9Ke/+a5wBI464eDRo3eF0LcfpWg=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3 h1:yxUGvEatvDMO6gkhwx82Va+Czdyui9LiCw6a5YB/2f8=
|
||||
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3/go.mod h1:1Q04m7wmv/IMoZU9t8UkH+n9McWn4i3H9v9LnMgqloo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 h1:hBSHahWMEgzwRyS6dRpxY0XyjZsHyQ61s084wo5PJe0=
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
|
||||
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 h1:umElSU9WZirRdgu2yFHY0ayQkEnKiOC1TtM3fWXFnoU=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/gormigrate.v1 v1.6.0 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI=
|
||||
gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
moul.io/srand v1.6.1 h1:SJ335F+54ivLdlH7wH52Rtyv0Ffos6DpsF5wu3ZVMXU=
|
||||
moul.io/srand v1.6.1/go.mod h1:P2uaZB+GFstFNo8sEj6/U8FRV1n25kD0LLckFpJ+qvc=
|
||||
73
healthcheck.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// perform a healthcheck test without requiring an ssh client or an ssh key (used for Docker's HEALTHCHECK)
|
||||
func healthcheck(addr string, wait, quiet bool) error {
|
||||
cfg := gossh.ClientConfig{
|
||||
User: "healthcheck",
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key gossh.PublicKey) error { return nil },
|
||||
Auth: []gossh.AuthMethod{gossh.Password("healthcheck")},
|
||||
}
|
||||
|
||||
if wait {
|
||||
for {
|
||||
if err := healthcheckOnce(addr, cfg, quiet); err != nil {
|
||||
if !quiet {
|
||||
log.Printf("error: %v", err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := healthcheckOnce(addr, cfg, quiet); err != nil {
|
||||
if quiet {
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func healthcheckOnce(addr string, config gossh.ClientConfig, quiet bool) error {
|
||||
client, err := gossh.Dial("tcp", addr, &config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := session.Close(); err != nil {
|
||||
if !quiet {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var b bytes.Buffer
|
||||
session.Stdout = &b
|
||||
if err := session.Run(""); err != nil {
|
||||
return err
|
||||
}
|
||||
stdout := strings.TrimSpace(b.String())
|
||||
if stdout != "OK" {
|
||||
return fmt.Errorf("invalid stdout: %q expected 'OK'", stdout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
22
helm/sshportal/.helmignore
Normal file
@@ -0,0 +1,22 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
21
helm/sshportal/Chart.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
apiVersion: v2
|
||||
name: sshportal
|
||||
description: A Helm chart for SSHPortal on Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application.
|
||||
appVersion: 1.10.0
|
||||
33
helm/sshportal/templates/NOTES.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
1. Get the admin invitation token (only on first install):
|
||||
export INVITE=$(kubectl --namespace sshportal logs -l "app.kubernetes.io/name={{ include "sshportal.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" | grep -Eo "invite:[a-zA-Z0-9]+")
|
||||
|
||||
2. Get the service IP and Port:
|
||||
{{- if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sshportal.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sshportal.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sshportal.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sshportal.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 2222:{{ .Values.service.port }}
|
||||
{{- end }}
|
||||
|
||||
3. Enroll your SSH public key:
|
||||
{{- if contains "NodePort" .Values.service.type }}
|
||||
ssh $NODE_IP -p $NODE_PORT -l $INVITE
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
ssh $SERVICE_IP -p {{ .Values.service.port }} -l $INVITE
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
ssh localhost -p 2222 -l $INVITE
|
||||
{{- end }}
|
||||
|
||||
4. Configure your {{ include "sshportal.name" . }} install:
|
||||
{{- if contains "NodePort" .Values.service.type }}
|
||||
ssh admin@$NODE_IP -p $NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
ssh admin@$SERVICE_IP -p {{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
ssh admin@localhost -p 2222
|
||||
{{- end }}
|
||||
63
helm/sshportal/templates/_helpers.tpl
Normal file
@@ -0,0 +1,63 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "sshportal.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "sshportal.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "sshportal.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "sshportal.labels" -}}
|
||||
helm.sh/chart: {{ include "sshportal.chart" . }}
|
||||
{{ include "sshportal.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "sshportal.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "sshportal.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "sshportal.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
{{ default (include "sshportal.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else -}}
|
||||
{{ default "default" .Values.serviceAccount.name }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
69
helm/sshportal/templates/deployment.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "sshportal.fullname" . }}
|
||||
labels:
|
||||
{{- include "sshportal.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "sshportal.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "sshportal.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:v{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: ssh
|
||||
containerPort: 2222
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sshportal
|
||||
- healthcheck
|
||||
- --quiet
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sshportal
|
||||
- healthcheck
|
||||
- --quiet
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.mysql.enabled }}
|
||||
- name: SSHPORTAL_DATABASE_URL
|
||||
value: {{ .Values.mysql.user }}:{{ .Values.mysql.password }}@tcp({{ .Values.mysql.server }}:{{ .Values.mysql.port }})/{{ .Values.mysql.database }}?charset=utf8&parseTime=true&loc=Local
|
||||
- name: SSHPORTAL_DB_DRIVER
|
||||
value: mysql
|
||||
{{- end }}
|
||||
{{- if .Values.debug}}
|
||||
- name: SSHPORTAL_DEBUG
|
||||
value: "1"
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
21
helm/sshportal/templates/horizontal-pod-autoscaling.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
{{- if .Values.mysql.enabled }}
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "sshportal.fullname" . }}
|
||||
labels:
|
||||
{{- include "sshportal.labels" . | nindent 4 }}
|
||||
spec:
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "sshportal.fullname" . }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: {{ .Values.autoscaling.cpuTarget }}
|
||||
{{- end }}
|
||||
|
||||
17
helm/sshportal/templates/service.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "sshportal.fullname" . }}
|
||||
annotations:
|
||||
{{- toYaml .Values.service.annotations | nindent 4 }}
|
||||
labels:
|
||||
{{- include "sshportal.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: 2222
|
||||
protocol: TCP
|
||||
name: ssh
|
||||
selector:
|
||||
{{- include "sshportal.selectorLabels" . | nindent 4 }}
|
||||
15
helm/sshportal/templates/tests/test-connection.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "sshportal.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{ include "sshportal.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test-success
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "sshportal.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
119
helm/sshportal/values.yaml
Normal file
@@ -0,0 +1,119 @@
|
||||
# Default values for sshportal.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
## Enable SSHPortal debug mode
|
||||
##
|
||||
debug: false
|
||||
|
||||
## SSH Portal Docker image
|
||||
##
|
||||
image:
|
||||
repository: moul/sshportal
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
## Reference to one or more secrets to be used when pulling images
|
||||
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
##
|
||||
imagePullSecrets: []
|
||||
|
||||
## Provide a name in place of sshportal for `app:` labels
|
||||
##
|
||||
nameOverride: ""
|
||||
|
||||
## Provide a name to substitute for the full names of resources
|
||||
##
|
||||
fullnameOverride: ""
|
||||
|
||||
## PodSecurityContext holds pod-level security attributes.
|
||||
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
|
||||
##
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
## SecurityContext holds container-level security attributes.
|
||||
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
|
||||
##
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
## Service
|
||||
##
|
||||
service:
|
||||
## Configure additional annotations for SSHPortal service
|
||||
##
|
||||
annotations: {}
|
||||
# service.beta.kubernetes.io/openstack-internal-load-balancer: "true"
|
||||
|
||||
## Service type, one of
|
||||
## NodePort, ClusterIP, LoadBalancer
|
||||
##
|
||||
type: LoadBalancer
|
||||
|
||||
## Port to expose on the service
|
||||
##
|
||||
port: 22
|
||||
|
||||
## Define resources requests and limits
|
||||
## ref: https://kubernetes.io/docs/user-guide/compute-resources/
|
||||
##
|
||||
resources: {}
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# limits:
|
||||
# cpu: 2
|
||||
# memory: 2Gi
|
||||
|
||||
## Mysql/MariaDB configuration for HA
|
||||
##
|
||||
mysql:
|
||||
enabled: false
|
||||
|
||||
## Database user
|
||||
##
|
||||
user: sshportal
|
||||
|
||||
## Database password
|
||||
##
|
||||
password: change_me
|
||||
|
||||
## Database name
|
||||
##
|
||||
database: sshportal
|
||||
|
||||
## Database server FQDN or IP
|
||||
##
|
||||
server: mariadb-mariadb-galera
|
||||
|
||||
## Database port
|
||||
##
|
||||
port: 3306
|
||||
|
||||
## Define which Nodes the Pods are scheduled on.
|
||||
## ref: https://kubernetes.io/docs/user-guide/node-selection/
|
||||
##
|
||||
nodeSelector: {}
|
||||
|
||||
## The pod's tolerations.
|
||||
## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
|
||||
##
|
||||
tolerations: []
|
||||
|
||||
## Assign custom affinity rules
|
||||
## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/
|
||||
##
|
||||
affinity: {}
|
||||
|
||||
## HPA support, require `mysql.enable: true`
|
||||
## This section enables sshportal to autoscale based on metrics.
|
||||
##
|
||||
autoscaling:
|
||||
maxReplicas: 4
|
||||
minReplicas: 2
|
||||
cpuTarget: 60
|
||||
302
main.go
@@ -1,240 +1,114 @@
|
||||
package main
|
||||
package main // import "moul.io/sshportal"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/urfave/cli"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"moul.io/srand"
|
||||
)
|
||||
|
||||
var (
|
||||
// VERSION should be updated by hand at each release
|
||||
VERSION = "1.2.0"
|
||||
// GIT_TAG will be overwritten automatically by the build system
|
||||
GIT_TAG string
|
||||
// GIT_SHA will be overwritten automatically by the build system
|
||||
GIT_SHA string
|
||||
// GIT_BRANCH will be overwritten automatically by the build system
|
||||
GIT_BRANCH string
|
||||
)
|
||||
|
||||
type sshportalContextKey string
|
||||
|
||||
var (
|
||||
userContextKey = sshportalContextKey("user")
|
||||
messageContextKey = sshportalContextKey("message")
|
||||
errorContextKey = sshportalContextKey("error")
|
||||
// GitTag will be overwritten automatically by the build system
|
||||
GitTag = "n/a"
|
||||
// GitSha will be overwritten automatically by the build system
|
||||
GitSha = "n/a"
|
||||
)
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rand.Seed(srand.MustSecure())
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = path.Base(os.Args[0])
|
||||
app.Author = "Manfred Touron"
|
||||
app.Version = VERSION + " (" + GIT_SHA + ")"
|
||||
app.Email = "https://github.com/moul/sshportal"
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "bind-address, b",
|
||||
EnvVar: "SSHPORTAL_BIND",
|
||||
Value: ":2222",
|
||||
Usage: "SSH server bind address",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "demo",
|
||||
Usage: "*unsafe* - demo mode: accept all connections",
|
||||
},
|
||||
/*cli.StringFlag{
|
||||
Name: "db-driver",
|
||||
Value: "sqlite3",
|
||||
Usage: "GORM driver (sqlite3)",
|
||||
},*/
|
||||
cli.StringFlag{
|
||||
Name: "db-conn",
|
||||
Value: "./sshportal.db",
|
||||
Usage: "GORM connection string",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug, D",
|
||||
Usage: "Display debug information",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "config-user",
|
||||
Usage: "SSH user that spawns a configuration shell",
|
||||
Value: "admin",
|
||||
app.Version = GitTag + " (" + GitSha + ")"
|
||||
app.Email = "https://moul.io/sshportal"
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "server",
|
||||
Usage: "Start sshportal server",
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := ensureLogDirectory(c.String("logs-location")); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := parseServerConfig(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return server(cfg)
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "bind-address, b",
|
||||
EnvVar: "SSHPORTAL_BIND",
|
||||
Value: ":2222",
|
||||
Usage: "SSH server bind address",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "db-driver",
|
||||
EnvVar: "SSHPORTAL_DB_DRIVER",
|
||||
Value: "sqlite3",
|
||||
Usage: "GORM driver (sqlite3)",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "db-conn",
|
||||
EnvVar: "SSHPORTAL_DATABASE_URL",
|
||||
Value: "./sshportal.db",
|
||||
Usage: "GORM connection string",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug, D",
|
||||
EnvVar: "SSHPORTAL_DEBUG",
|
||||
Usage: "Display debug information",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "aes-key",
|
||||
EnvVar: "SSHPORTAL_AES_KEY",
|
||||
Usage: "Encrypt sensitive data in database (length: 16, 24 or 32)",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "logs-location",
|
||||
EnvVar: "SSHPORTAL_LOGS_LOCATION",
|
||||
Value: "./log",
|
||||
Usage: "Store user session files",
|
||||
},
|
||||
cli.DurationFlag{
|
||||
Name: "idle-timeout",
|
||||
Value: 0,
|
||||
Usage: "Duration before an inactive connection is timed out (0 to disable)",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: "healthcheck",
|
||||
Action: func(c *cli.Context) error { return healthcheck(c.String("addr"), c.Bool("wait"), c.Bool("quiet")) },
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "addr, a",
|
||||
Value: "localhost:2222",
|
||||
Usage: "sshportal server address",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "wait, w",
|
||||
Usage: "Loop indefinitely until sshportal is ready",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "quiet, q",
|
||||
Usage: "Do not print errors, if any",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: "_test_server",
|
||||
Hidden: true,
|
||||
Action: testServer,
|
||||
},
|
||||
}
|
||||
app.Action = server
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Fatalf("error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func server(c *cli.Context) error {
|
||||
// db
|
||||
db, err := gorm.Open("sqlite3", c.String("db-conn"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
if err = db.DB().Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Bool("debug") {
|
||||
db.LogMode(true)
|
||||
}
|
||||
if err := dbInit(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Bool("demo") {
|
||||
if err := dbDemo(db); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// ssh server
|
||||
ssh.Handle(func(s ssh.Session) {
|
||||
currentUser := s.Context().Value(userContextKey).(User)
|
||||
log.Printf("New connection: sshUser=%q remote=%q local=%q command=%q dbUser=id:%q,email:%s", s.User(), s.RemoteAddr(), s.LocalAddr(), s.Command(), currentUser.ID, currentUser.Email)
|
||||
|
||||
if err := s.Context().Value(errorContextKey); err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if msg := s.Context().Value(messageContextKey); msg != nil {
|
||||
fmt.Fprint(s, msg.(string))
|
||||
}
|
||||
|
||||
switch username := s.User(); {
|
||||
case username == c.String("config-user"):
|
||||
if !currentUser.IsAdmin {
|
||||
fmt.Fprintf(s, "You are not an administrator, permission denied.\n")
|
||||
return
|
||||
}
|
||||
if err := shell(c, s, s.Command(), db); err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
}
|
||||
case strings.HasPrefix(username, "invite:"):
|
||||
return
|
||||
default:
|
||||
host, err := RemoteHostFromSession(s, db)
|
||||
if err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
// FIXME: print available hosts
|
||||
return
|
||||
}
|
||||
|
||||
// load up-to-date objects
|
||||
// FIXME: cache them or try not to load them
|
||||
var tmpUser User
|
||||
if err := db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", currentUser.ID).First(&tmpUser).Error; err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
return
|
||||
}
|
||||
var tmpHost Host
|
||||
if err := db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", host.ID).First(&tmpHost).Error; err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
action, err := CheckACLs(tmpUser, tmpHost)
|
||||
if err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "allow":
|
||||
if err := proxy(s, host); err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
}
|
||||
case "deny":
|
||||
fmt.Fprintf(s, "You don't have permission to that host.\n")
|
||||
default:
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
opts := []ssh.Option{}
|
||||
opts = append(opts, ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
var (
|
||||
userKey UserKey
|
||||
user User
|
||||
username = ctx.User()
|
||||
)
|
||||
|
||||
// lookup user by key
|
||||
db.Where("key = ?", key.Marshal()).First(&userKey)
|
||||
if userKey.UserID > 0 {
|
||||
db.Where("id = ?", userKey.UserID).First(&user)
|
||||
if strings.HasPrefix(username, "invite:") {
|
||||
ctx.SetValue(errorContextKey, fmt.Errorf("invites are only supported for ney SSH keys; your ssh key is already associated with the user %q.", user.Email))
|
||||
}
|
||||
ctx.SetValue(userContextKey, user)
|
||||
return true
|
||||
}
|
||||
|
||||
// handle invite "links"
|
||||
if strings.HasPrefix(username, "invite:") {
|
||||
inputToken := strings.Split(username, ":")[1]
|
||||
if len(inputToken) > 0 {
|
||||
db.Where("invite_token = ?", inputToken).First(&user)
|
||||
}
|
||||
if user.ID > 0 {
|
||||
userKey = UserKey{
|
||||
UserID: user.ID,
|
||||
Key: key.Marshal(),
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
db.Create(&userKey)
|
||||
|
||||
// token is only usable once
|
||||
user.InviteToken = ""
|
||||
db.Update(&user)
|
||||
|
||||
ctx.SetValue(messageContextKey, fmt.Sprintf("Welcome %s!\n\nYour key is now associated with the user %q.\n", user.Name, user.Email))
|
||||
ctx.SetValue(userContextKey, user)
|
||||
} else {
|
||||
ctx.SetValue(userContextKey, User{Name: "Anonymous"})
|
||||
ctx.SetValue(errorContextKey, errors.New("your token is invalid or expired"))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// fallback
|
||||
ctx.SetValue(errorContextKey, errors.New("unknown ssh key"))
|
||||
ctx.SetValue(userContextKey, User{Name: "Anonymous"})
|
||||
return true
|
||||
}))
|
||||
|
||||
opts = append(opts, func(srv *ssh.Server) error {
|
||||
key, err := FindKeyByIdOrName(db, "host")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signer, err := gossh.ParsePrivateKey([]byte(key.PrivKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.AddHostKey(signer)
|
||||
return nil
|
||||
})
|
||||
|
||||
log.Printf("SSH Server accepting connections on %s", c.String("bind-address"))
|
||||
return ssh.ListenAndServe(c.String("bind-address"), nil, opts...)
|
||||
}
|
||||
|
||||
50
pkg/bastion/acl.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package bastion
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
type byWeight []*dbmodels.ACL
|
||||
|
||||
func (a byWeight) Len() int { return len(a) }
|
||||
func (a byWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byWeight) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
|
||||
|
||||
func checkACLs(user dbmodels.User, host dbmodels.Host) string {
|
||||
currentTime := time.Now()
|
||||
|
||||
// shared ACLs between user and host
|
||||
aclMap := map[uint]*dbmodels.ACL{}
|
||||
for _, userGroup := range user.Groups {
|
||||
for _, userGroupACL := range userGroup.ACLs {
|
||||
for _, hostGroup := range host.Groups {
|
||||
for _, hostGroupACL := range hostGroup.ACLs {
|
||||
if userGroupACL.ID == hostGroupACL.ID {
|
||||
if (userGroupACL.Inception == nil || currentTime.After(*userGroupACL.Inception)) &&
|
||||
(userGroupACL.Expiration == nil || currentTime.Before(*userGroupACL.Expiration)) {
|
||||
aclMap[userGroupACL.ID] = userGroupACL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// FIXME: add ACLs that match host pattern
|
||||
|
||||
// deny by default if no shared ACL
|
||||
if len(aclMap) == 0 {
|
||||
return string(dbmodels.ACLActionDeny) // default action
|
||||
}
|
||||
|
||||
// transform map to slice and sort it
|
||||
acls := make([]*dbmodels.ACL, 0, len(aclMap))
|
||||
for _, acl := range aclMap {
|
||||
acls = append(acls, acl)
|
||||
}
|
||||
sort.Sort(byWeight(acls))
|
||||
|
||||
return acls[0].Action
|
||||
}
|
||||
49
pkg/bastion/acl_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
func TestCheckACLs(t *testing.T) {
|
||||
Convey("Testing CheckACLs", t, func(c C) {
|
||||
// create tmp dir
|
||||
tempDir, err := ioutil.TempDir("", "sshportal")
|
||||
c.So(err, ShouldBeNil)
|
||||
defer func() {
|
||||
c.So(os.RemoveAll(tempDir), ShouldBeNil)
|
||||
}()
|
||||
|
||||
// create sqlite db
|
||||
db, err := gorm.Open("sqlite3", filepath.Join(tempDir, "sshportal.db"))
|
||||
c.So(err, ShouldBeNil)
|
||||
db.LogMode(false)
|
||||
c.So(DBInit(db), ShouldBeNil)
|
||||
|
||||
// create dummy objects
|
||||
var hostGroup dbmodels.HostGroup
|
||||
err = dbmodels.HostGroupsByIdentifiers(db, []string{"default"}).First(&hostGroup).Error
|
||||
c.So(err, ShouldBeNil)
|
||||
db.Create(&dbmodels.Host{Groups: []*dbmodels.HostGroup{&hostGroup}})
|
||||
|
||||
//. load db
|
||||
var (
|
||||
hosts []dbmodels.Host
|
||||
users []dbmodels.User
|
||||
)
|
||||
db.Preload("Groups").Preload("Groups.ACLs").Find(&hosts)
|
||||
db.Preload("Groups").Preload("Groups.ACLs").Find(&users)
|
||||
|
||||
// test
|
||||
action := checkACLs(users[0], hosts[0])
|
||||
c.So(action, ShouldEqual, dbmodels.ACLActionAllow)
|
||||
})
|
||||
}
|
||||
697
pkg/bastion/dbinit.go
Normal file
@@ -0,0 +1,697 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
gormigrate "gopkg.in/gormigrate.v1"
|
||||
"moul.io/sshportal/pkg/crypto"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
func DBInit(db *gorm.DB) error {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
db.Callback().Delete().Replace("gorm:delete", hardDeleteCallback)
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
|
||||
{
|
||||
ID: "1",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Setting struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
return tx.AutoMigrate(&Setting{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("settings").Error
|
||||
},
|
||||
}, {
|
||||
ID: "2",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type SSHKey struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Type string
|
||||
Length uint
|
||||
Fingerprint string
|
||||
PrivKey string `sql:"size:5000"`
|
||||
PubKey string `sql:"size:1000"`
|
||||
Hosts []*dbmodels.Host `gorm:"ForeignKey:SSHKeyID"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&SSHKey{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("ssh_keys").Error
|
||||
},
|
||||
}, {
|
||||
ID: "3",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32"`
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Fingerprint string
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("hosts").Error
|
||||
},
|
||||
}, {
|
||||
ID: "4",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type UserKey struct {
|
||||
gorm.Model
|
||||
Key []byte `sql:"size:1000"`
|
||||
UserID uint ``
|
||||
User *dbmodels.User `gorm:"ForeignKey:UserID"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&UserKey{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("user_keys").Error
|
||||
},
|
||||
}, {
|
||||
ID: "5",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type User struct {
|
||||
gorm.Model
|
||||
IsAdmin bool
|
||||
Email string
|
||||
Name string
|
||||
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string
|
||||
InviteToken string
|
||||
}
|
||||
return tx.AutoMigrate(&User{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("users").Error
|
||||
},
|
||||
}, {
|
||||
ID: "6",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type UserGroup struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Users []*dbmodels.User `gorm:"many2many:user_user_groups;"`
|
||||
ACLs []*dbmodels.ACL `gorm:"many2many:user_group_acls;"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&UserGroup{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("user_groups").Error
|
||||
},
|
||||
}, {
|
||||
ID: "7",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type HostGroup struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Hosts []*dbmodels.Host `gorm:"many2many:host_host_groups;"`
|
||||
ACLs []*dbmodels.ACL `gorm:"many2many:host_group_acls;"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&HostGroup{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("host_groups").Error
|
||||
},
|
||||
}, {
|
||||
ID: "8",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type ACL struct {
|
||||
gorm.Model
|
||||
HostGroups []*dbmodels.HostGroup `gorm:"many2many:host_group_acls;"`
|
||||
UserGroups []*dbmodels.UserGroup `gorm:"many2many:user_group_acls;"`
|
||||
HostPattern string
|
||||
Action string
|
||||
Weight uint
|
||||
Comment string
|
||||
}
|
||||
|
||||
return tx.AutoMigrate(&ACL{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("acls").Error
|
||||
},
|
||||
}, {
|
||||
ID: "9",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&dbmodels.Setting{}).RemoveIndex("uix_settings_name")
|
||||
return db.Model(&dbmodels.Setting{}).AddUniqueIndex("uix_settings_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&dbmodels.Setting{}).RemoveIndex("uix_settings_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "10",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&dbmodels.SSHKey{}).RemoveIndex("uix_keys_name")
|
||||
return db.Model(&dbmodels.SSHKey{}).AddUniqueIndex("uix_keys_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&dbmodels.SSHKey{}).RemoveIndex("uix_keys_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "11",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&dbmodels.Host{}).RemoveIndex("uix_hosts_name")
|
||||
return db.Model(&dbmodels.Host{}).AddUniqueIndex("uix_hosts_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&dbmodels.Host{}).RemoveIndex("uix_hosts_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "12",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&dbmodels.User{}).RemoveIndex("uix_users_name")
|
||||
return db.Model(&dbmodels.User{}).AddUniqueIndex("uix_users_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&dbmodels.User{}).RemoveIndex("uix_users_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "13",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&dbmodels.UserGroup{}).RemoveIndex("uix_usergroups_name")
|
||||
return db.Model(&dbmodels.UserGroup{}).AddUniqueIndex("uix_usergroups_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&dbmodels.UserGroup{}).RemoveIndex("uix_usergroups_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "14",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&dbmodels.HostGroup{}).RemoveIndex("uix_hostgroups_name")
|
||||
return db.Model(&dbmodels.HostGroup{}).AddUniqueIndex("uix_hostgroups_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&dbmodels.HostGroup{}).RemoveIndex("uix_hostgroups_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "15",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type UserRole struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Users []*dbmodels.User `gorm:"many2many:user_user_roles"`
|
||||
}
|
||||
return tx.AutoMigrate(&UserRole{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("user_roles").Error
|
||||
},
|
||||
}, {
|
||||
ID: "16",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type User struct {
|
||||
gorm.Model
|
||||
IsAdmin bool
|
||||
Roles []*dbmodels.UserRole `gorm:"many2many:user_user_roles"`
|
||||
Email string `valid:"required,email"`
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string `valid:"optional"`
|
||||
InviteToken string `valid:"optional,length(10|60)"`
|
||||
}
|
||||
return tx.AutoMigrate(&User{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "17",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Create(&dbmodels.UserRole{Name: "admin"}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Where("name = ?", "admin").Delete(&dbmodels.UserRole{}).Error
|
||||
},
|
||||
}, {
|
||||
ID: "18",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var adminRole dbmodels.UserRole
|
||||
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var users []*dbmodels.User
|
||||
if err := db.Preload("Roles").Where("is_admin = ?", true).Find(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
user.Roles = append(user.Roles, &adminRole)
|
||||
if err := tx.Save(user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "19",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Roles []*dbmodels.UserRole `gorm:"many2many:user_user_roles"`
|
||||
Email string `valid:"required,email"`
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string `valid:"optional"`
|
||||
InviteToken string `valid:"optional,length(10|60)"`
|
||||
}
|
||||
return tx.AutoMigrate(&User{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "20",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Create(&dbmodels.UserRole{Name: "listhosts"}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Where("name = ?", "listhosts").Delete(&dbmodels.UserRole{}).Error
|
||||
},
|
||||
}, {
|
||||
ID: "21",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Session struct {
|
||||
gorm.Model
|
||||
StoppedAt time.Time `valid:"optional"`
|
||||
Status string `valid:"required"`
|
||||
User *dbmodels.User `gorm:"ForeignKey:UserID"`
|
||||
Host *dbmodels.Host `gorm:"ForeignKey:HostID"`
|
||||
UserID uint `valid:"optional"`
|
||||
HostID uint `valid:"optional"`
|
||||
ErrMsg string `valid:"optional"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
return tx.AutoMigrate(&Session{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("sessions").Error
|
||||
},
|
||||
}, {
|
||||
ID: "22",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Event struct {
|
||||
gorm.Model
|
||||
Author *dbmodels.User `gorm:"ForeignKey:AuthorID"`
|
||||
AuthorID uint `valid:"optional"`
|
||||
Domain string `valid:"required"`
|
||||
Action string `valid:"required"`
|
||||
Entity string `valid:"optional"`
|
||||
Args []byte `sql:"size:10000" valid:"optional,length(1|10000)"`
|
||||
}
|
||||
return tx.AutoMigrate(&Event{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.DropTable("events").Error
|
||||
},
|
||||
}, {
|
||||
ID: "23",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type UserKey struct {
|
||||
gorm.Model
|
||||
Key []byte `sql:"size:1000" valid:"required,length(1|1000)"`
|
||||
AuthorizedKey string `sql:"size:1000" valid:"required,length(1|1000)"`
|
||||
UserID uint ``
|
||||
User *dbmodels.User `gorm:"ForeignKey:UserID"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
return tx.AutoMigrate(&UserKey{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "24",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var userKeys []*dbmodels.UserKey
|
||||
if err := db.Find(&userKeys).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, userKey := range userKeys {
|
||||
key, err := gossh.ParsePublicKey(userKey.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userKey.AuthorizedKey = string(gossh.MarshalAuthorizedKey(key))
|
||||
if err := db.Model(userKey).Updates(userKey).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "25",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
|
||||
Addr string `valid:"required"`
|
||||
User string `valid:"optional"`
|
||||
Password string `valid:"optional"`
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:1000" valid:"optional"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Fingerprint string `valid:"optional"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "26",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Session struct {
|
||||
gorm.Model
|
||||
StoppedAt *time.Time `sql:"index" valid:"optional"`
|
||||
Status string `valid:"required"`
|
||||
User *dbmodels.User `gorm:"ForeignKey:UserID"`
|
||||
Host *dbmodels.Host `gorm:"ForeignKey:HostID"`
|
||||
UserID uint `valid:"optional"`
|
||||
HostID uint `valid:"optional"`
|
||||
ErrMsg string `valid:"optional"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
return tx.AutoMigrate(&Session{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "27",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var sessions []*dbmodels.Session
|
||||
if err := db.Find(&sessions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, session := range sessions {
|
||||
if session.StoppedAt != nil && session.StoppedAt.IsZero() {
|
||||
if err := db.Model(session).Updates(map[string]interface{}{"stopped_at": nil}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "28",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32"`
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
URL string
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:1000"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "29",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32"`
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
URL string
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:1000"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string
|
||||
Hop *dbmodels.Host
|
||||
HopID uint
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "30",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32"`
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
URL string
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string
|
||||
Hop *dbmodels.Host
|
||||
Logging string
|
||||
HopID uint
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error { return fmt.Errorf("not implemented") },
|
||||
}, {
|
||||
ID: "31",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Model(&dbmodels.Host{}).Updates(&dbmodels.Host{Logging: "everything"}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error { return fmt.Errorf("not implemented") },
|
||||
}, {
|
||||
ID: "32",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type ACL struct {
|
||||
gorm.Model
|
||||
HostGroups []*dbmodels.HostGroup `gorm:"many2many:host_group_acls;"`
|
||||
UserGroups []*dbmodels.UserGroup `gorm:"many2many:user_group_acls;"`
|
||||
HostPattern string `valid:"optional"`
|
||||
Action string `valid:"required"`
|
||||
Weight uint ``
|
||||
Comment string `valid:"optional"`
|
||||
Inception *time.Time
|
||||
Expiration *time.Time
|
||||
}
|
||||
return tx.AutoMigrate(&ACL{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error { return fmt.Errorf("not implemented") },
|
||||
},
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
return err
|
||||
}
|
||||
dbmodels.NewEvent("system", "migrated").Log(db)
|
||||
|
||||
// create default ssh key
|
||||
var count uint
|
||||
if err := db.Table("ssh_keys").Where("name = ?", "default").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
key, err := crypto.NewSSHKey("ed25519", 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.Name = "default"
|
||||
key.Comment = "created by sshportal"
|
||||
if err := db.Create(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create default host group
|
||||
if err := db.Table("host_groups").Where("name = ?", "default").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
hostGroup := dbmodels.HostGroup{
|
||||
Name: "default",
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
if err := db.Create(&hostGroup).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create default user group
|
||||
if err := db.Table("user_groups").Where("name = ?", "default").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
userGroup := dbmodels.UserGroup{
|
||||
Name: "default",
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
if err := db.Create(&userGroup).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create default acl
|
||||
if err := db.Table("acls").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
var defaultUserGroup dbmodels.UserGroup
|
||||
db.Where("name = ?", "default").First(&defaultUserGroup)
|
||||
var defaultHostGroup dbmodels.HostGroup
|
||||
db.Where("name = ?", "default").First(&defaultHostGroup)
|
||||
acl := dbmodels.ACL{
|
||||
UserGroups: []*dbmodels.UserGroup{&defaultUserGroup},
|
||||
HostGroups: []*dbmodels.HostGroup{&defaultHostGroup},
|
||||
Action: "allow",
|
||||
//HostPattern: "",
|
||||
//Weight: 0,
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
if err := db.Create(&acl).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create admin user
|
||||
var defaultUserGroup dbmodels.UserGroup
|
||||
db.Where("name = ?", "default").First(&defaultUserGroup)
|
||||
if err := db.Table("users").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
// if no admin, create an account for the first connection
|
||||
inviteToken := randStringBytes(16)
|
||||
if os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN") != "" {
|
||||
inviteToken = os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN")
|
||||
}
|
||||
var adminRole dbmodels.UserRole
|
||||
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var username string
|
||||
if currentUser, err := user.Current(); err == nil {
|
||||
username = currentUser.Username
|
||||
}
|
||||
if username == "" {
|
||||
username = os.Getenv("USER")
|
||||
}
|
||||
username = strings.ToLower(username)
|
||||
if username == "" {
|
||||
username = "admin" // fallback username
|
||||
}
|
||||
user := dbmodels.User{
|
||||
Name: username,
|
||||
Email: fmt.Sprintf("%s@localhost", username),
|
||||
Comment: "created by sshportal",
|
||||
Roles: []*dbmodels.UserRole{&adminRole},
|
||||
InviteToken: inviteToken,
|
||||
Groups: []*dbmodels.UserGroup{&defaultUserGroup},
|
||||
}
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("info 'admin' user created, use the user 'invite:%s' to associate a public key with this account", user.InviteToken)
|
||||
}
|
||||
|
||||
// create host ssh key
|
||||
if err := db.Table("ssh_keys").Where("name = ?", "host").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
key, err := crypto.NewSSHKey("ed25519", 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.Name = "host"
|
||||
key.Comment = "created by sshportal"
|
||||
if err := db.Create(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// close unclosed connections
|
||||
return db.Table("sessions").Where("status = ?", "active").Updates(&dbmodels.Session{
|
||||
Status: string(dbmodels.SessionStatusClosed),
|
||||
ErrMsg: "sshportal was halted while the connection was still active",
|
||||
}).Error
|
||||
}
|
||||
|
||||
func hardDeleteCallback(scope *gorm.Scope) {
|
||||
if !scope.HasError() {
|
||||
var extraOption string
|
||||
if str, ok := scope.Get("gorm:delete_option"); ok {
|
||||
extraOption = fmt.Sprint(str)
|
||||
}
|
||||
|
||||
/* #nosec */
|
||||
scope.Raw(fmt.Sprintf(
|
||||
"DELETE FROM %v%v%v",
|
||||
scope.QuotedTableName(),
|
||||
addExtraSpaceIfExist(scope.CombinedConditionSql()),
|
||||
addExtraSpaceIfExist(extraOption),
|
||||
)).Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func addExtraSpaceIfExist(str string) string {
|
||||
if str != "" {
|
||||
return " " + str
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func randStringBytes(n int) string {
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
71
pkg/bastion/logtunnel.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type logTunnel struct {
|
||||
host string
|
||||
channel ssh.Channel
|
||||
writer io.WriteCloser
|
||||
}
|
||||
|
||||
type logTunnelForwardData struct {
|
||||
DestinationHost string
|
||||
DestinationPort uint32
|
||||
SourceHost string
|
||||
SourcePort uint32
|
||||
}
|
||||
|
||||
func writeHeader(fd io.Writer, length int) {
|
||||
t := time.Now()
|
||||
|
||||
tv := syscall.NsecToTimeval(t.UnixNano())
|
||||
|
||||
if err := binary.Write(fd, binary.LittleEndian, int32(tv.Sec)); err != nil {
|
||||
log.Printf("failed to write log header: %v", err)
|
||||
}
|
||||
if err := binary.Write(fd, binary.LittleEndian, tv.Usec); err != nil {
|
||||
log.Printf("failed to write log header: %v", err)
|
||||
}
|
||||
if err := binary.Write(fd, binary.LittleEndian, int32(length)); err != nil {
|
||||
log.Printf("failed to write log header: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newLogTunnel(channel ssh.Channel, writer io.WriteCloser, host string) io.ReadWriteCloser {
|
||||
return &logTunnel{
|
||||
host: host,
|
||||
channel: channel,
|
||||
writer: writer,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logTunnel) Read(data []byte) (int, error) {
|
||||
return 0, errors.New("logTunnel.Read is not implemented")
|
||||
}
|
||||
|
||||
func (l *logTunnel) Write(data []byte) (int, error) {
|
||||
writeHeader(l.writer, len(data)+len(l.host+": "))
|
||||
if _, err := l.writer.Write([]byte(l.host + ": ")); err != nil {
|
||||
log.Printf("failed to write log: %v", err)
|
||||
}
|
||||
if _, err := l.writer.Write(data); err != nil {
|
||||
log.Printf("failed to write log: %v", err)
|
||||
}
|
||||
|
||||
return l.channel.Write(data)
|
||||
}
|
||||
|
||||
func (l *logTunnel) Close() error {
|
||||
l.writer.Close()
|
||||
|
||||
return l.channel.Close()
|
||||
}
|
||||
264
pkg/bastion/session.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sabban/bastion/pkg/logchannel"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type sessionConfig struct {
|
||||
Addr string
|
||||
LogsLocation string
|
||||
ClientConfig *gossh.ClientConfig
|
||||
LoggingMode string
|
||||
}
|
||||
|
||||
func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []sessionConfig, sessionID uint) error {
|
||||
var lastClient *gossh.Client
|
||||
switch newChan.ChannelType() {
|
||||
case "session":
|
||||
lch, lreqs, err := newChan.Accept()
|
||||
// TODO: defer clean closer
|
||||
if err != nil {
|
||||
// TODO: trigger event callback
|
||||
return nil
|
||||
}
|
||||
|
||||
// go through all the hops
|
||||
for _, config := range configs {
|
||||
var client *gossh.Client
|
||||
if lastClient == nil {
|
||||
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
|
||||
} else {
|
||||
rconn, err := lastClient.Dial("tcp", config.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ncc, chans, reqs, err := gossh.NewClientConn(rconn, config.Addr, config.ClientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client = gossh.NewClient(ncc, chans, reqs)
|
||||
}
|
||||
if err != nil {
|
||||
lch.Close() // fix #56
|
||||
return err
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
lastClient = client
|
||||
}
|
||||
|
||||
rch, rreqs, err := lastClient.OpenChannel("session", []byte{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := conn.User()
|
||||
actx := ctx.Value(authContextKey).(*authContext)
|
||||
username := actx.user.Name
|
||||
// pipe everything
|
||||
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1], user, username, sessionID, newChan)
|
||||
case "direct-tcpip":
|
||||
lch, lreqs, err := newChan.Accept()
|
||||
// TODO: defer clean closer
|
||||
if err != nil {
|
||||
// TODO: trigger event callback
|
||||
return nil
|
||||
}
|
||||
|
||||
// go through all the hops
|
||||
for _, config := range configs {
|
||||
var client *gossh.Client
|
||||
if lastClient == nil {
|
||||
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
|
||||
} else {
|
||||
rconn, err := lastClient.Dial("tcp", config.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ncc, chans, reqs, err := gossh.NewClientConn(rconn, config.Addr, config.ClientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client = gossh.NewClient(ncc, chans, reqs)
|
||||
}
|
||||
if err != nil {
|
||||
lch.Close()
|
||||
return err
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
lastClient = client
|
||||
}
|
||||
|
||||
d := logTunnelForwardData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
return err
|
||||
}
|
||||
rch, rreqs, err := lastClient.OpenChannel("direct-tcpip", newChan.ExtraData())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := conn.User()
|
||||
actx := ctx.Value(authContextKey).(*authContext)
|
||||
username := actx.user.Name
|
||||
// pipe everything
|
||||
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1], user, username, sessionID, newChan)
|
||||
default:
|
||||
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
|
||||
log.Printf("failed to reject chan: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, sessConfig sessionConfig, user string, username string, sessionID uint, newChan gossh.NewChannel) error {
|
||||
defer func() {
|
||||
_ = lch.Close()
|
||||
_ = rch.Close()
|
||||
}()
|
||||
|
||||
errch := make(chan error, 1)
|
||||
quit := make(chan string, 1)
|
||||
channeltype := newChan.ChannelType()
|
||||
|
||||
var logWriter io.WriteCloser = newDiscardWriteCloser()
|
||||
if sessConfig.LoggingMode != "disabled" {
|
||||
filename := filepath.Join(sessConfig.LogsLocation, fmt.Sprintf("%s-%s-%s-%d-%s", user, username, channeltype, sessionID, time.Now().Format(time.RFC3339)))
|
||||
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0440)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "open log file")
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
log.Printf("Session %v is recorded in %v", channeltype, filename)
|
||||
logWriter = f
|
||||
}
|
||||
|
||||
if channeltype == "session" {
|
||||
switch sessConfig.LoggingMode {
|
||||
case "input":
|
||||
wrappedrch := logchannel.New(rch, logWriter)
|
||||
go func(quit chan string) {
|
||||
_, _ = io.Copy(lch, rch)
|
||||
quit <- "rch"
|
||||
}(quit)
|
||||
go func(quit chan string) {
|
||||
_, _ = io.Copy(wrappedrch, lch)
|
||||
quit <- "lch"
|
||||
}(quit)
|
||||
default: // everything, disabled
|
||||
wrappedlch := logchannel.New(lch, logWriter)
|
||||
go func(quit chan string) {
|
||||
_, _ = io.Copy(wrappedlch, rch)
|
||||
quit <- "rch"
|
||||
}(quit)
|
||||
go func(quit chan string) {
|
||||
_, _ = io.Copy(rch, lch)
|
||||
quit <- "lch"
|
||||
}(quit)
|
||||
}
|
||||
}
|
||||
if channeltype == "direct-tcpip" {
|
||||
d := logTunnelForwardData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
return err
|
||||
}
|
||||
wrappedlch := newLogTunnel(lch, logWriter, d.SourceHost)
|
||||
wrappedrch := newLogTunnel(rch, logWriter, d.DestinationHost)
|
||||
go func(quit chan string) {
|
||||
_, _ = io.Copy(wrappedlch, rch)
|
||||
quit <- "rch"
|
||||
}(quit)
|
||||
|
||||
go func(quit chan string) {
|
||||
_, _ = io.Copy(wrappedrch, lch)
|
||||
quit <- "lch"
|
||||
}(quit)
|
||||
}
|
||||
|
||||
go func(quit chan string) {
|
||||
for req := range lreqs {
|
||||
b, err := rch.SendRequest(req.Type, req.WantReply, req.Payload)
|
||||
if req.Type == "exec" {
|
||||
wrappedlch := logchannel.New(lch, logWriter)
|
||||
command := append(req.Payload, []byte("\n")...)
|
||||
if _, err := wrappedlch.LogWrite(command); err != nil {
|
||||
log.Printf("failed to write log: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errch <- err
|
||||
}
|
||||
if err2 := req.Reply(b, nil); err2 != nil {
|
||||
errch <- err2
|
||||
}
|
||||
}
|
||||
quit <- "lreqs"
|
||||
}(quit)
|
||||
|
||||
go func(quit chan string) {
|
||||
for req := range rreqs {
|
||||
b, err := lch.SendRequest(req.Type, req.WantReply, req.Payload)
|
||||
if err != nil {
|
||||
errch <- err
|
||||
}
|
||||
if err2 := req.Reply(b, nil); err2 != nil {
|
||||
errch <- err2
|
||||
}
|
||||
}
|
||||
quit <- "rreqs"
|
||||
}(quit)
|
||||
|
||||
lchEOF, rchEOF, lchClosed, rchClosed := false, false, false, false
|
||||
for {
|
||||
select {
|
||||
case err := <-errch:
|
||||
return err
|
||||
case q := <-quit:
|
||||
switch q {
|
||||
case "lch":
|
||||
lchEOF = true
|
||||
_ = rch.CloseWrite()
|
||||
case "rch":
|
||||
rchEOF = true
|
||||
_ = lch.CloseWrite()
|
||||
case "lreqs":
|
||||
lchClosed = true
|
||||
case "rreqs":
|
||||
rchClosed = true
|
||||
}
|
||||
|
||||
if lchEOF && lchClosed && !rchClosed {
|
||||
rch.Close()
|
||||
}
|
||||
|
||||
if rchEOF && rchClosed && !lchClosed {
|
||||
lch.Close()
|
||||
}
|
||||
|
||||
if lchEOF && rchEOF && lchClosed && rchClosed {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newDiscardWriteCloser() io.WriteCloser { return &discardWriteCloser{ioutil.Discard} }
|
||||
|
||||
type discardWriteCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (discardWriteCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
2390
pkg/bastion/shell.go
Normal file
348
pkg/bastion/ssh.go
Normal file
@@ -0,0 +1,348 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"moul.io/sshportal/pkg/crypto"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
type sshportalContextKey string
|
||||
|
||||
var authContextKey = sshportalContextKey("auth")
|
||||
|
||||
type authContext struct {
|
||||
message string
|
||||
err error
|
||||
user dbmodels.User
|
||||
inputUsername string
|
||||
db *gorm.DB
|
||||
userKey dbmodels.UserKey
|
||||
logsLocation string
|
||||
aesKey string
|
||||
dbDriver, dbURL string
|
||||
bindAddr string
|
||||
demo, debug bool
|
||||
authMethod string
|
||||
authSuccess bool
|
||||
}
|
||||
|
||||
type userType string
|
||||
|
||||
const (
|
||||
userTypeHealthcheck userType = "healthcheck"
|
||||
userTypeBastion userType = "bastion"
|
||||
userTypeInvite userType = "invite"
|
||||
userTypeShell userType = "shell"
|
||||
)
|
||||
|
||||
func (c authContext) userType() userType {
|
||||
switch {
|
||||
case c.inputUsername == "healthcheck":
|
||||
return userTypeHealthcheck
|
||||
case c.inputUsername == c.user.Name || c.inputUsername == c.user.Email || c.inputUsername == "admin":
|
||||
return userTypeShell
|
||||
case strings.HasPrefix(c.inputUsername, "invite:"):
|
||||
return userTypeInvite
|
||||
default:
|
||||
return userTypeBastion
|
||||
}
|
||||
}
|
||||
|
||||
func dynamicHostKey(db *gorm.DB, host *dbmodels.Host) gossh.HostKeyCallback {
|
||||
return func(hostname string, remote net.Addr, key gossh.PublicKey) error {
|
||||
if len(host.HostKey) == 0 {
|
||||
log.Println("Discovering host fingerprint...")
|
||||
return db.Model(host).Update("HostKey", key.Marshal()).Error
|
||||
}
|
||||
|
||||
if !bytes.Equal(host.HostKey, key.Marshal()) {
|
||||
return fmt.Errorf("ssh: host key mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var DefaultChannelHandler ssh.ChannelHandler = func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {}
|
||||
|
||||
func ChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
|
||||
switch newChan.ChannelType() {
|
||||
case "session":
|
||||
case "direct-tcpip":
|
||||
default:
|
||||
// TODO: handle direct-tcp (only for ssh scheme)
|
||||
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
|
||||
log.Printf("error: failed to reject channel: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
actx := ctx.Value(authContextKey).(*authContext)
|
||||
|
||||
switch actx.userType() {
|
||||
case userTypeBastion:
|
||||
log.Printf("New connection(bastion): sshUser=%q remote=%q local=%q dbUser=id:%d,email:%s", conn.User(), conn.RemoteAddr(), conn.LocalAddr(), actx.user.ID, actx.user.Email)
|
||||
host, err := dbmodels.HostByName(actx.db, actx.inputUsername)
|
||||
if err != nil {
|
||||
ch, _, err2 := newChan.Accept()
|
||||
if err2 != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(ch, "error: %v\n", err)
|
||||
// FIXME: force close all channels
|
||||
_ = ch.Close()
|
||||
return
|
||||
}
|
||||
|
||||
switch host.Scheme() {
|
||||
case dbmodels.BastionSchemeSSH:
|
||||
sessionConfigs := make([]sessionConfig, 0)
|
||||
currentHost := host
|
||||
for currentHost != nil {
|
||||
clientConfig, err2 := bastionClientConfig(ctx, currentHost)
|
||||
if err2 != nil {
|
||||
ch, _, err3 := newChan.Accept()
|
||||
if err3 != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(ch, "error: %v\n", err2)
|
||||
// FIXME: force close all channels
|
||||
_ = ch.Close()
|
||||
return
|
||||
}
|
||||
sessionConfigs = append([]sessionConfig{{
|
||||
Addr: currentHost.DialAddr(),
|
||||
ClientConfig: clientConfig,
|
||||
LogsLocation: actx.logsLocation,
|
||||
LoggingMode: currentHost.Logging,
|
||||
}}, sessionConfigs...)
|
||||
if currentHost.HopID != 0 {
|
||||
var newHost dbmodels.Host
|
||||
actx.db.Model(currentHost).Related(&newHost, "HopID")
|
||||
hostname := newHost.Name
|
||||
currentHost, _ = dbmodels.HostByName(actx.db, hostname)
|
||||
} else {
|
||||
currentHost = nil
|
||||
}
|
||||
}
|
||||
|
||||
sess := dbmodels.Session{
|
||||
UserID: actx.user.ID,
|
||||
HostID: host.ID,
|
||||
Status: string(dbmodels.SessionStatusActive),
|
||||
}
|
||||
if err = actx.db.Create(&sess).Error; err != nil {
|
||||
ch, _, err2 := newChan.Accept()
|
||||
if err2 != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(ch, "error: %v\n", err)
|
||||
_ = ch.Close()
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
err = multiChannelHandler(conn, newChan, ctx, sessionConfigs, sess.ID)
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
sessUpdate := dbmodels.Session{
|
||||
Status: string(dbmodels.SessionStatusClosed),
|
||||
ErrMsg: fmt.Sprintf("%v", err),
|
||||
StoppedAt: &now,
|
||||
}
|
||||
if err == nil {
|
||||
sessUpdate.ErrMsg = ""
|
||||
}
|
||||
actx.db.Model(&sess).Updates(&sessUpdate)
|
||||
}()
|
||||
case dbmodels.BastionSchemeTelnet:
|
||||
tmpSrv := ssh.Server{
|
||||
// PtyCallback: srv.PtyCallback,
|
||||
Handler: telnetHandler(host),
|
||||
}
|
||||
DefaultChannelHandler(&tmpSrv, conn, newChan, ctx)
|
||||
default:
|
||||
ch, _, err2 := newChan.Accept()
|
||||
if err2 != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(ch, "error: unknown bastion scheme: %q\n", host.Scheme())
|
||||
// FIXME: force close all channels
|
||||
_ = ch.Close()
|
||||
}
|
||||
default: // shell
|
||||
DefaultChannelHandler(srv, conn, newChan, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func bastionClientConfig(ctx ssh.Context, host *dbmodels.Host) (*gossh.ClientConfig, error) {
|
||||
actx := ctx.Value(authContextKey).(*authContext)
|
||||
|
||||
crypto.HostDecrypt(actx.aesKey, host)
|
||||
crypto.SSHKeyDecrypt(actx.aesKey, host.SSHKey)
|
||||
|
||||
clientConfig, err := host.ClientConfig(dynamicHostKey(actx.db, host))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tmpUser dbmodels.User
|
||||
if err = actx.db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", actx.user.ID).First(&tmpUser).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tmpHost dbmodels.Host
|
||||
if err = actx.db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", host.ID).First(&tmpHost).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
action := checkACLs(tmpUser, tmpHost)
|
||||
switch action {
|
||||
case string(dbmodels.ACLActionAllow):
|
||||
// do nothing
|
||||
case string(dbmodels.ACLActionDeny):
|
||||
return nil, fmt.Errorf("you don't have permission to that host")
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid ACL action: %q", action)
|
||||
}
|
||||
return clientConfig, nil
|
||||
}
|
||||
|
||||
func ShellHandler(s ssh.Session, version, gitSha, gitTag string) {
|
||||
actx := s.Context().Value(authContextKey).(*authContext)
|
||||
if actx.userType() != userTypeHealthcheck {
|
||||
log.Printf("New connection(shell): sshUser=%q remote=%q local=%q command=%q dbUser=id:%d,email:%s", s.User(), s.RemoteAddr(), s.LocalAddr(), s.Command(), actx.user.ID, actx.user.Email)
|
||||
}
|
||||
|
||||
if actx.err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", actx.err)
|
||||
_ = s.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
if actx.message != "" {
|
||||
fmt.Fprint(s, actx.message)
|
||||
}
|
||||
|
||||
switch actx.userType() {
|
||||
case userTypeHealthcheck:
|
||||
fmt.Fprintln(s, "OK")
|
||||
return
|
||||
case userTypeShell:
|
||||
if err := shell(s, version, gitSha, gitTag); err != nil {
|
||||
fmt.Fprintf(s, "error: %v\n", err)
|
||||
_ = s.Exit(1)
|
||||
}
|
||||
return
|
||||
case userTypeInvite:
|
||||
// do nothing (message was printed at the beginning of the function)
|
||||
return
|
||||
}
|
||||
panic("should not happen")
|
||||
}
|
||||
|
||||
func PasswordAuthHandler(db *gorm.DB, logsLocation, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PasswordHandler {
|
||||
return func(ctx ssh.Context, pass string) bool {
|
||||
actx := &authContext{
|
||||
db: db,
|
||||
inputUsername: ctx.User(),
|
||||
logsLocation: logsLocation,
|
||||
aesKey: aesKey,
|
||||
dbDriver: dbDriver,
|
||||
dbURL: dbURL,
|
||||
bindAddr: bindAddr,
|
||||
demo: demo,
|
||||
authMethod: "password",
|
||||
}
|
||||
actx.authSuccess = actx.userType() == userTypeHealthcheck
|
||||
ctx.SetValue(authContextKey, actx)
|
||||
return actx.authSuccess
|
||||
}
|
||||
}
|
||||
|
||||
func PrivateKeyFromDB(db *gorm.DB, aesKey string) func(*ssh.Server) error {
|
||||
return func(srv *ssh.Server) error {
|
||||
var key dbmodels.SSHKey
|
||||
if err := dbmodels.SSHKeysByIdentifiers(db, []string{"host"}).First(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
crypto.SSHKeyDecrypt(aesKey, &key)
|
||||
|
||||
signer, err := gossh.ParsePrivateKey([]byte(key.PrivKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.AddHostKey(signer)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func PublicKeyAuthHandler(db *gorm.DB, logsLocation, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PublicKeyHandler {
|
||||
return func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
actx := &authContext{
|
||||
db: db,
|
||||
inputUsername: ctx.User(),
|
||||
logsLocation: logsLocation,
|
||||
aesKey: aesKey,
|
||||
dbDriver: dbDriver,
|
||||
dbURL: dbURL,
|
||||
bindAddr: bindAddr,
|
||||
demo: demo,
|
||||
authMethod: "pubkey",
|
||||
authSuccess: true,
|
||||
}
|
||||
ctx.SetValue(authContextKey, actx)
|
||||
|
||||
// lookup user by key
|
||||
db.Where("authorized_key = ?", string(gossh.MarshalAuthorizedKey(key))).First(&actx.userKey)
|
||||
if actx.userKey.UserID > 0 {
|
||||
db.Preload("Roles").Where("id = ?", actx.userKey.UserID).First(&actx.user)
|
||||
if actx.userType() == userTypeInvite {
|
||||
actx.err = fmt.Errorf("invites are only supported for new SSH keys; your ssh key is already associated with the user %q", actx.user.Email)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// handle invite "links"
|
||||
if actx.userType() == userTypeInvite {
|
||||
inputToken := strings.Split(actx.inputUsername, ":")[1]
|
||||
if len(inputToken) > 0 {
|
||||
db.Where("invite_token = ?", inputToken).First(&actx.user)
|
||||
}
|
||||
if actx.user.ID > 0 {
|
||||
actx.userKey = dbmodels.UserKey{
|
||||
UserID: actx.user.ID,
|
||||
Key: key.Marshal(),
|
||||
Comment: "created by sshportal",
|
||||
AuthorizedKey: string(gossh.MarshalAuthorizedKey(key)),
|
||||
}
|
||||
db.Create(&actx.userKey)
|
||||
|
||||
// token is only usable once
|
||||
actx.user.InviteToken = ""
|
||||
db.Model(&actx.user).Updates(&actx.user)
|
||||
|
||||
actx.message = fmt.Sprintf("Welcome %s!\n\nYour key is now associated with the user %q.\n", actx.user.Name, actx.user.Email)
|
||||
} else {
|
||||
actx.user = dbmodels.User{Name: "Anonymous"}
|
||||
actx.err = errors.New("your token is invalid or expired")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// fallback
|
||||
actx.err = errors.New("unknown ssh key")
|
||||
actx.user = dbmodels.User{Name: "Anonymous"}
|
||||
return true
|
||||
}
|
||||
}
|
||||
88
pkg/bastion/telnet.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
oi "github.com/reiver/go-oi"
|
||||
telnet "github.com/reiver/go-telnet"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
type bastionTelnetCaller struct {
|
||||
ssh ssh.Session
|
||||
}
|
||||
|
||||
func (caller bastionTelnetCaller) CallTELNET(ctx telnet.Context, w telnet.Writer, r telnet.Reader) {
|
||||
go func(writer io.Writer, reader io.Reader) {
|
||||
var buffer [1]byte // Seems like the length of the buffer needs to be small, otherwise will have to wait for buffer to fill up.
|
||||
p := buffer[:]
|
||||
|
||||
for {
|
||||
// Read 1 byte.
|
||||
n, err := reader.Read(p)
|
||||
if n <= 0 && err == nil {
|
||||
continue
|
||||
} else if n <= 0 && err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if _, err = oi.LongWrite(writer, p); err != nil {
|
||||
log.Printf("telnet longwrite failed: %v", err)
|
||||
}
|
||||
}
|
||||
}(caller.ssh, r)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
var p []byte
|
||||
|
||||
var crlfBuffer = [2]byte{'\r', '\n'}
|
||||
crlf := crlfBuffer[:]
|
||||
|
||||
scanner := bufio.NewScanner(caller.ssh)
|
||||
scanner.Split(scannerSplitFunc)
|
||||
|
||||
for scanner.Scan() {
|
||||
buffer.Write(scanner.Bytes())
|
||||
buffer.Write(crlf)
|
||||
|
||||
p = buffer.Bytes()
|
||||
|
||||
n, err := oi.LongWrite(w, p)
|
||||
if nil != err {
|
||||
break
|
||||
}
|
||||
if expected, actual := int64(len(p)), n; expected != actual {
|
||||
err := fmt.Errorf("transmission problem: tried sending %d bytes, but actually only sent %d bytes", expected, actual)
|
||||
fmt.Fprint(caller.ssh, err.Error())
|
||||
return
|
||||
}
|
||||
buffer.Reset()
|
||||
}
|
||||
|
||||
// Wait a bit to receive data from the server (that we would send to io.Stdout).
|
||||
time.Sleep(3 * time.Millisecond)
|
||||
}
|
||||
|
||||
func scannerSplitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF {
|
||||
return 0, nil, nil
|
||||
}
|
||||
return bufio.ScanLines(data, atEOF)
|
||||
}
|
||||
|
||||
func telnetHandler(host *dbmodels.Host) ssh.Handler {
|
||||
return func(s ssh.Session) {
|
||||
// FIXME: log session in db
|
||||
//actx := s.Context().Value(authContextKey).(*authContext)
|
||||
caller := bastionTelnetCaller{ssh: s}
|
||||
if err := telnet.DialToAndCall(host.DialAddr(), caller); err != nil {
|
||||
fmt.Fprintf(s, "error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
242
pkg/crypto/crypto.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package crypto // import "moul.io/sshportal/pkg/crypto"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
func NewSSHKey(keyType string, length uint) (*dbmodels.SSHKey, error) {
|
||||
key := dbmodels.SSHKey{
|
||||
Type: keyType,
|
||||
Length: length,
|
||||
}
|
||||
|
||||
// generate the private key
|
||||
var err error
|
||||
var pemKey *pem.Block
|
||||
var publicKey gossh.PublicKey
|
||||
switch keyType {
|
||||
case "rsa":
|
||||
pemKey, publicKey, err = NewRSAKey(length)
|
||||
case "ecdsa":
|
||||
pemKey, publicKey, err = NewECDSAKey(length)
|
||||
case "ed25519":
|
||||
pemKey, publicKey, err = NewEd25519Key()
|
||||
default:
|
||||
return nil, fmt.Errorf("key type not supported: %q, supported types are: rsa, ecdsa, ed25519", key.Type)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := bytes.NewBufferString("")
|
||||
if err = pem.Encode(buf, pemKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key.PrivKey = buf.String()
|
||||
|
||||
// generate authorized-key formatted pubkey output
|
||||
key.PubKey = strings.TrimSpace(string(gossh.MarshalAuthorizedKey(publicKey)))
|
||||
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func NewRSAKey(length uint) (*pem.Block, gossh.PublicKey, error) {
|
||||
if length < 1024 || length > 16384 {
|
||||
return nil, nil, fmt.Errorf("key length not supported: %d, supported values are between 1024 and 16384", length)
|
||||
}
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, int(length))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// convert priv key to x509 format
|
||||
pemKey := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||
}
|
||||
publicKey, err := gossh.NewPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return pemKey, publicKey, err
|
||||
}
|
||||
|
||||
func NewECDSAKey(length uint) (*pem.Block, gossh.PublicKey, error) {
|
||||
var curve elliptic.Curve
|
||||
switch length {
|
||||
case 256:
|
||||
curve = elliptic.P256()
|
||||
case 384:
|
||||
curve = elliptic.P384()
|
||||
case 521:
|
||||
curve = elliptic.P521()
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("key length not supported: %d, supported values are 256, 384, 521", length)
|
||||
}
|
||||
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// convert priv key to x509 format
|
||||
marshaledKey, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
pemKey := &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: marshaledKey,
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
publicKey, err := gossh.NewPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return pemKey, publicKey, err
|
||||
}
|
||||
|
||||
func NewEd25519Key() (*pem.Block, gossh.PublicKey, error) {
|
||||
publicKeyEd25519, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// convert priv key to x509 format
|
||||
marshaledKey, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
pemKey := &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: marshaledKey,
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
publicKey, err := gossh.NewPublicKey(publicKeyEd25519)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return pemKey, publicKey, err
|
||||
}
|
||||
|
||||
func ImportSSHKey(keyValue string) (*dbmodels.SSHKey, error) {
|
||||
key := dbmodels.SSHKey{
|
||||
Type: "rsa",
|
||||
}
|
||||
|
||||
parsedKey, err := gossh.ParseRawPrivateKey([]byte(keyValue))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var privateKey *rsa.PrivateKey
|
||||
var ok bool
|
||||
if privateKey, ok = parsedKey.(*rsa.PrivateKey); !ok {
|
||||
return nil, errors.New("key type not supported")
|
||||
}
|
||||
key.Length = uint(privateKey.PublicKey.N.BitLen())
|
||||
// convert priv key to x509 format
|
||||
var pemKey = &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||
}
|
||||
buf := bytes.NewBufferString("")
|
||||
if err = pem.Encode(buf, pemKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key.PrivKey = buf.String()
|
||||
|
||||
// generte authorized-key formatted pubkey output
|
||||
pub, err := gossh.NewPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key.PubKey = strings.TrimSpace(string(gossh.MarshalAuthorizedKey(pub)))
|
||||
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func encrypt(key []byte, text string) (string, error) {
|
||||
plaintext := []byte(text)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
|
||||
return base64.URLEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
func decrypt(key []byte, cryptoText string) (string, error) {
|
||||
ciphertext, _ := base64.URLEncoding.DecodeString(cryptoText)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return "", fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext, ciphertext)
|
||||
return fmt.Sprintf("%s", ciphertext), nil
|
||||
}
|
||||
|
||||
func safeDecrypt(key []byte, cryptoText string) string {
|
||||
if len(key) == 0 {
|
||||
return cryptoText
|
||||
}
|
||||
out, err := decrypt(key, cryptoText)
|
||||
if err != nil {
|
||||
return cryptoText
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func HostEncrypt(aesKey string, host *dbmodels.Host) (err error) {
|
||||
if aesKey == "" {
|
||||
return nil
|
||||
}
|
||||
if host.Password != "" {
|
||||
host.Password, err = encrypt([]byte(aesKey), host.Password)
|
||||
}
|
||||
return
|
||||
}
|
||||
func HostDecrypt(aesKey string, host *dbmodels.Host) {
|
||||
if aesKey == "" {
|
||||
return
|
||||
}
|
||||
if host.Password != "" {
|
||||
host.Password = safeDecrypt([]byte(aesKey), host.Password)
|
||||
}
|
||||
}
|
||||
|
||||
func SSHKeyEncrypt(aesKey string, key *dbmodels.SSHKey) (err error) {
|
||||
if aesKey == "" {
|
||||
return nil
|
||||
}
|
||||
key.PrivKey, err = encrypt([]byte(aesKey), key.PrivKey)
|
||||
return
|
||||
}
|
||||
func SSHKeyDecrypt(aesKey string, key *dbmodels.SSHKey) {
|
||||
if aesKey == "" {
|
||||
return
|
||||
}
|
||||
key.PrivKey = safeDecrypt([]byte(aesKey), key.PrivKey)
|
||||
}
|
||||
472
pkg/dbmodels/dbmodels.go
Normal file
@@ -0,0 +1,472 @@
|
||||
package dbmodels // import "moul.io/sshportal/pkg/dbmodels"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
SSHKeys []*SSHKey `json:"keys"`
|
||||
Hosts []*Host `json:"hosts"`
|
||||
UserKeys []*UserKey `json:"user_keys"`
|
||||
Users []*User `json:"users"`
|
||||
UserGroups []*UserGroup `json:"user_groups"`
|
||||
HostGroups []*HostGroup `json:"host_groups"`
|
||||
ACLs []*ACL `json:"acls"`
|
||||
Settings []*Setting `json:"settings"`
|
||||
Events []*Event `json:"events"`
|
||||
Sessions []*Session `json:"sessions"`
|
||||
// FIXME: add latest migration
|
||||
Date time.Time `json:"date"`
|
||||
}
|
||||
|
||||
type Setting struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required"`
|
||||
Value string `valid:"required"`
|
||||
}
|
||||
|
||||
// SSHKey defines a ssh client key (used by sshportal to connect to remote hosts)
|
||||
type SSHKey struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|255),unix_user"`
|
||||
Type string `valid:"required"`
|
||||
Length uint `valid:"required"`
|
||||
Fingerprint string `valid:"optional"`
|
||||
PrivKey string `sql:"size:5000" valid:"required"`
|
||||
PubKey string `sql:"size:1000" valid:"optional"`
|
||||
Hosts []*Host `gorm:"ForeignKey:SSHKeyID"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type Host struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `gorm:"size:255" valid:"required,length(1|255)"`
|
||||
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:1000" valid:"optional"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string `valid:"optional"`
|
||||
Logging string `valid:"optional,host_logging_mode"`
|
||||
Hop *Host
|
||||
HopID uint
|
||||
}
|
||||
|
||||
// UserKey defines a user public key used by sshportal to identify the user
|
||||
type UserKey struct {
|
||||
gorm.Model
|
||||
Key []byte `sql:"size:1000" valid:"length(1|1000)"`
|
||||
AuthorizedKey string `sql:"size:1000" valid:"required,length(1|1000)"`
|
||||
UserID uint ``
|
||||
User *User `gorm:"ForeignKey:UserID"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type UserRole struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|255),unix_user"`
|
||||
Users []*User `gorm:"many2many:user_user_roles"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Roles []*UserRole `gorm:"many2many:user_user_roles"`
|
||||
Email string `valid:"required,email"`
|
||||
Name string `valid:"required,length(1|255),unix_user"`
|
||||
Keys []*UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string `valid:"optional"`
|
||||
InviteToken string `valid:"optional,length(10|60)"`
|
||||
}
|
||||
|
||||
type UserGroup struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|255),unix_user"`
|
||||
Users []*User `gorm:"many2many:user_user_groups;"`
|
||||
ACLs []*ACL `gorm:"many2many:user_group_acls;"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type HostGroup struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|255),unix_user"`
|
||||
Hosts []*Host `gorm:"many2many:host_host_groups;"`
|
||||
ACLs []*ACL `gorm:"many2many:host_group_acls;"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type ACL struct {
|
||||
gorm.Model
|
||||
HostGroups []*HostGroup `gorm:"many2many:host_group_acls;"`
|
||||
UserGroups []*UserGroup `gorm:"many2many:user_group_acls;"`
|
||||
HostPattern string `valid:"optional"`
|
||||
Action string `valid:"required"`
|
||||
Weight uint ``
|
||||
Comment string `valid:"optional"`
|
||||
Inception *time.Time
|
||||
Expiration *time.Time
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
gorm.Model
|
||||
StoppedAt *time.Time `sql:"index" valid:"optional"`
|
||||
Status string `valid:"required"`
|
||||
User *User `gorm:"ForeignKey:UserID"`
|
||||
Host *Host `gorm:"ForeignKey:HostID"`
|
||||
UserID uint `valid:"optional"`
|
||||
HostID uint `valid:"optional"`
|
||||
ErrMsg string `valid:"optional"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
gorm.Model
|
||||
Author *User `gorm:"ForeignKey:AuthorID"`
|
||||
AuthorID uint `valid:"optional"`
|
||||
Domain string `valid:"required"`
|
||||
Action string `valid:"required"`
|
||||
Entity string `valid:"optional"`
|
||||
Args []byte `sql:"size:10000" valid:"optional,length(1|10000)" json:"-"`
|
||||
ArgsMap map[string]interface{} `gorm:"-" json:"Args"`
|
||||
}
|
||||
|
||||
type SessionStatus string
|
||||
|
||||
const (
|
||||
SessionStatusUnknown SessionStatus = "unknown"
|
||||
SessionStatusActive SessionStatus = "active"
|
||||
SessionStatusClosed SessionStatus = "closed"
|
||||
)
|
||||
|
||||
type ACLAction string
|
||||
|
||||
const (
|
||||
ACLActionAllow ACLAction = "allow"
|
||||
ACLActionDeny ACLAction = "deny"
|
||||
)
|
||||
|
||||
type BastionScheme string
|
||||
|
||||
const (
|
||||
BastionSchemeSSH BastionScheme = "ssh"
|
||||
BastionSchemeTelnet BastionScheme = "telnet"
|
||||
)
|
||||
|
||||
// Generic Helper
|
||||
func GenericNameOrID(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
var ids []string
|
||||
var names []string
|
||||
for _, s := range identifiers {
|
||||
if _, err := strconv.Atoi(s); err == nil {
|
||||
ids = append(ids, s)
|
||||
} else {
|
||||
names = append(names, s)
|
||||
}
|
||||
}
|
||||
if len(ids) > 0 && len(names) > 0 {
|
||||
return db.Where("id IN (?)", ids).Or("name IN (?)", names)
|
||||
} else if len(ids) > 0 {
|
||||
return db.Where("id IN (?)", ids)
|
||||
}
|
||||
return db.Where("name IN (?)", names)
|
||||
}
|
||||
|
||||
// Host helpers
|
||||
|
||||
func (host *Host) DialAddr() string {
|
||||
return fmt.Sprintf("%s:%d", host.Hostname(), host.Port())
|
||||
}
|
||||
func (host *Host) String() string {
|
||||
if host.URL != "" {
|
||||
return host.URL
|
||||
} else if host.Addr != "" { // to be removed in a future version in favor of URL
|
||||
if host.Password != "" {
|
||||
return fmt.Sprintf("ssh://%s:%s@%s", host.User, strings.Repeat("*", 4), host.Addr)
|
||||
}
|
||||
return fmt.Sprintf("ssh://%s@%s", host.User, host.Addr)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (host *Host) Scheme() BastionScheme {
|
||||
if host.URL != "" {
|
||||
u, err := url.Parse(host.URL)
|
||||
if err != nil {
|
||||
return BastionSchemeSSH
|
||||
}
|
||||
return BastionScheme(u.Scheme)
|
||||
} else if host.Addr != "" {
|
||||
return BastionSchemeSSH
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (host *Host) Hostname() string {
|
||||
if host.URL != "" {
|
||||
u, err := url.Parse(host.URL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return u.Hostname()
|
||||
} else if host.Addr != "" { // to be removed in a future version in favor of URL
|
||||
return strings.Split(host.Addr, ":")[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (host *Host) Username() string {
|
||||
if host.URL != "" {
|
||||
u, err := url.Parse(host.URL)
|
||||
if err != nil {
|
||||
return "root"
|
||||
}
|
||||
if u.User != nil {
|
||||
return u.User.Username()
|
||||
}
|
||||
} else if host.User != "" { // to be removed in a future version in favor of URL
|
||||
return host.User
|
||||
}
|
||||
return "root"
|
||||
}
|
||||
func (host *Host) Passwd() string {
|
||||
if host.URL != "" {
|
||||
u, err := url.Parse(host.URL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if u.User != nil {
|
||||
password, _ := u.User.Password()
|
||||
return password
|
||||
}
|
||||
} else if host.Password != "" { // to be removed in a future version in favor of URL
|
||||
return host.Password
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (host *Host) Port() uint64 {
|
||||
var portString string
|
||||
if host.URL != "" {
|
||||
u, err := url.Parse(host.URL)
|
||||
if err != nil {
|
||||
goto defaultPort
|
||||
}
|
||||
portString = u.Port()
|
||||
} else if host.Addr != "" { // to be removed in a future version in favor of URL
|
||||
portString = strings.Split(host.Addr, ":")[1]
|
||||
}
|
||||
if portString != "" {
|
||||
port, err := strconv.ParseUint(portString, 10, 64)
|
||||
if err != nil {
|
||||
goto defaultPort
|
||||
}
|
||||
return port
|
||||
}
|
||||
defaultPort:
|
||||
switch host.Scheme() {
|
||||
case BastionSchemeSSH:
|
||||
return 22
|
||||
case BastionSchemeTelnet:
|
||||
return 23
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
func HostsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Groups").Preload("SSHKey")
|
||||
}
|
||||
func HostsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return GenericNameOrID(db, identifiers)
|
||||
}
|
||||
func HostByName(db *gorm.DB, name string) (*Host, error) {
|
||||
var host Host
|
||||
db.Preload("SSHKey").Where("name = ?", name).Find(&host)
|
||||
if host.Name == "" {
|
||||
// FIXME: add available hosts
|
||||
return nil, fmt.Errorf("no such target: %q", name)
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
func (host *Host) ClientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, error) {
|
||||
config := gossh.ClientConfig{
|
||||
User: host.Username(),
|
||||
HostKeyCallback: hk,
|
||||
Auth: []gossh.AuthMethod{},
|
||||
}
|
||||
if host.SSHKey != nil {
|
||||
signer, err := gossh.ParsePrivateKey([]byte(host.SSHKey.PrivKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Auth = append(config.Auth, gossh.PublicKeys(signer))
|
||||
}
|
||||
if host.Passwd() != "" {
|
||||
config.Auth = append(config.Auth, gossh.Password(host.Passwd()))
|
||||
}
|
||||
if len(config.Auth) == 0 {
|
||||
return nil, fmt.Errorf("no valid authentication method for host %q", host.Name)
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// SSHKey helpers
|
||||
|
||||
func SSHKeysPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Hosts")
|
||||
}
|
||||
func SSHKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return GenericNameOrID(db, identifiers)
|
||||
}
|
||||
|
||||
// HostGroup helpers
|
||||
|
||||
func HostGroupsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ACLs").Preload("Hosts")
|
||||
}
|
||||
func HostGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return GenericNameOrID(db, identifiers)
|
||||
}
|
||||
|
||||
// UserGroup helpers
|
||||
|
||||
func UserGroupsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ACLs").Preload("Users")
|
||||
}
|
||||
func UserGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return GenericNameOrID(db, identifiers)
|
||||
}
|
||||
|
||||
// User helpers
|
||||
|
||||
func UsersPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Groups").Preload("Keys").Preload("Roles")
|
||||
}
|
||||
func UsersByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
var ids []string
|
||||
var names []string
|
||||
for _, s := range identifiers {
|
||||
if _, err := strconv.Atoi(s); err == nil {
|
||||
ids = append(ids, s)
|
||||
} else {
|
||||
names = append(names, s)
|
||||
}
|
||||
}
|
||||
if len(ids) > 0 && len(names) > 0 {
|
||||
db.Where("id IN (?)", identifiers).Or("email IN (?)", identifiers).Or("name IN (?)", identifiers)
|
||||
} else if len(ids) > 0 {
|
||||
return db.Where("id IN (?)", ids)
|
||||
}
|
||||
return db.Where("email IN (?)", identifiers).Or("name IN (?)", identifiers)
|
||||
}
|
||||
func (u *User) HasRole(name string) bool {
|
||||
for _, role := range u.Roles {
|
||||
if role.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func (u *User) CheckRoles(names []string) error {
|
||||
for _, name := range names {
|
||||
if u.HasRole(name) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("you don't have permission to access this feature (requires any of these roles: '%s')", strings.Join(names, "', '"))
|
||||
}
|
||||
|
||||
// ACL helpers
|
||||
|
||||
func ACLsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("UserGroups").Preload("HostGroups")
|
||||
}
|
||||
func ACLsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// UserKey helpers
|
||||
|
||||
func UserKeysPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("User")
|
||||
}
|
||||
func UserKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers)
|
||||
}
|
||||
func UserKeysByUserID(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("user_id IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// UserRole helpers
|
||||
|
||||
//func UserRolesPreload(db *gorm.DB) *gorm.DB {
|
||||
// return db.Preload("Users")
|
||||
//}
|
||||
func UserRolesByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return GenericNameOrID(db, identifiers)
|
||||
}
|
||||
|
||||
// Session helpers
|
||||
|
||||
func SessionsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("User").Preload("Host")
|
||||
}
|
||||
func SessionsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers)
|
||||
}
|
||||
|
||||
// Events helpers
|
||||
|
||||
func EventsPreload(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Author")
|
||||
}
|
||||
func EventsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
||||
return db.Where("id IN (?)", identifiers)
|
||||
}
|
||||
|
||||
func NewEvent(domain, action string) *Event {
|
||||
return &Event{
|
||||
Domain: domain,
|
||||
Action: action,
|
||||
ArgsMap: map[string]interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Event) String() string {
|
||||
return fmt.Sprintf("%s %s %s %s", e.Domain, e.Action, e.Entity, string(e.Args))
|
||||
}
|
||||
|
||||
func (e *Event) Log(db *gorm.DB) {
|
||||
if len(e.ArgsMap) > 0 {
|
||||
var err error
|
||||
if e.Args, err = json.Marshal(e.ArgsMap); err != nil {
|
||||
log.Printf("error: %v", err)
|
||||
}
|
||||
}
|
||||
log.Printf("info: %s", e)
|
||||
if err := db.Create(e).Error; err != nil {
|
||||
log.Printf("warning: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Event) SetAuthor(user *User) *Event {
|
||||
//e.Author = user
|
||||
e.AuthorID = user.ID
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Event) SetArg(name string, value interface{}) *Event {
|
||||
e.ArgsMap[name] = value
|
||||
return e
|
||||
}
|
||||
33
pkg/dbmodels/validator.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package dbmodels
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
func InitValidator() {
|
||||
unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*")
|
||||
|
||||
govalidator.CustomTypeTagMap.Set("unix_user", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||
name, ok := i.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return unixUserRegexp.MatchString(name)
|
||||
}))
|
||||
govalidator.CustomTypeTagMap.Set("host_logging_mode", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||
name, ok := i.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if name == "" {
|
||||
return true
|
||||
}
|
||||
return IsValidHostLoggingMode(name)
|
||||
}))
|
||||
}
|
||||
|
||||
func IsValidHostLoggingMode(name string) bool {
|
||||
return name == "disabled" || name == "input" || name == "everything"
|
||||
}
|
||||
99
proxy.go
@@ -1,99 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func proxy(s ssh.Session, host *Host) error {
|
||||
config, err := host.ClientConfig(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rconn, err := gossh.Dial("tcp", host.Addr, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rconn.Close()
|
||||
|
||||
rch, rreqs, err := rconn.OpenChannel("session", []byte{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("SSH Connectin established")
|
||||
return pipe(s.MaskedReqs(), rreqs, s, rch)
|
||||
}
|
||||
|
||||
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel) error {
|
||||
defer func() {
|
||||
lch.Close()
|
||||
rch.Close()
|
||||
}()
|
||||
|
||||
errch := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(lch, rch)
|
||||
errch <- errors.New("lch closed the connection")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(rch, lch)
|
||||
errch <- errors.New("rch closed the connection")
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case req := <-lreqs: // forward ssh requests from local to remote
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := rch.SendRequest(req.Type, req.WantReply, req.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Reply(b, nil)
|
||||
case req := <-rreqs: // forward ssh requests from remote to local
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := lch.SendRequest(req.Type, req.WantReply, req.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Reply(b, nil)
|
||||
case err := <-errch:
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (host *Host) ClientConfig(_ ssh.Session) (*gossh.ClientConfig, error) {
|
||||
config := gossh.ClientConfig{
|
||||
User: host.User,
|
||||
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
|
||||
Auth: []gossh.AuthMethod{},
|
||||
}
|
||||
if host.SSHKey != nil {
|
||||
signer, err := gossh.ParsePrivateKey([]byte(host.SSHKey.PrivKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Auth = append(config.Auth, gossh.PublicKeys(signer))
|
||||
}
|
||||
if host.Password != "" {
|
||||
config.Auth = append(config.Auth, gossh.Password(host.Password))
|
||||
}
|
||||
if len(config.Auth) == 0 {
|
||||
return nil, fmt.Errorf("no valid authentication method for host %q", host.Name)
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
361
rules.mk
vendored
Normal file
@@ -0,0 +1,361 @@
|
||||
# +--------------------------------------------------------------+
|
||||
# | * * * moul.io/rules.mk |
|
||||
# +--------------------------------------------------------------+
|
||||
# | |
|
||||
# | ++ ______________________________________ |
|
||||
# | ++++ / \ |
|
||||
# | ++++ | | |
|
||||
# | ++++++++++ | https://moul.io/rules.mk is a set | |
|
||||
# | +++ | | of common Makefile rules that can | |
|
||||
# | ++ | | be configured from the Makefile | |
|
||||
# | + -== ==| | or with environment variables. | |
|
||||
# | ( <*> <*> | | |
|
||||
# | | | /| Manfred Touron | |
|
||||
# | | _) / | manfred.life | |
|
||||
# | | +++ / \______________________________________/ |
|
||||
# | \ =+ / |
|
||||
# | \ + |
|
||||
# | |\++++++ |
|
||||
# | | ++++ ||// |
|
||||
# | ___| |___ _||/__ __|
|
||||
# | / --- \ \| ||| __ _ ___ __ __/ /|
|
||||
# |/ | | \ \ / / ' \/ _ \/ // / / |
|
||||
# || | | | | | /_/_/_/\___/\_,_/_/ |
|
||||
# +--------------------------------------------------------------+
|
||||
|
||||
.PHONY: _default_entrypoint
|
||||
_default_entrypoint: help
|
||||
|
||||
##
|
||||
## Common helpers
|
||||
##
|
||||
|
||||
rwildcard = $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
|
||||
check-program = $(foreach exec,$(1),$(if $(shell PATH="$(PATH)" which $(exec)),,$(error "No $(exec) in PATH")))
|
||||
my-filter-out = $(foreach v,$(2),$(if $(findstring $(1),$(v)),,$(v)))
|
||||
novendor = $(call my-filter-out,vendor/,$(1))
|
||||
|
||||
##
|
||||
## rules.mk
|
||||
##
|
||||
ifneq ($(wildcard rules.mk),)
|
||||
.PHONY: rulesmk.bumpdeps
|
||||
rulesmk.bumpdeps:
|
||||
wget -O rules.mk https://raw.githubusercontent.com/moul/rules.mk/master/rules.mk
|
||||
BUMPDEPS_STEPS += rulesmk.bumpdeps
|
||||
endif
|
||||
|
||||
##
|
||||
## Maintainer
|
||||
##
|
||||
|
||||
ifneq ($(wildcard .git/HEAD),)
|
||||
.PHONY: generate.authors
|
||||
generate.authors: AUTHORS
|
||||
AUTHORS: .git/
|
||||
echo "# This file lists all individuals having contributed content to the repository." > AUTHORS
|
||||
echo "# For how it is generated, see 'https://github.com/moul/rules.mk'" >> AUTHORS
|
||||
echo >> AUTHORS
|
||||
git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf >> AUTHORS
|
||||
GENERATE_STEPS += generate.authors
|
||||
endif
|
||||
|
||||
##
|
||||
## Golang
|
||||
##
|
||||
|
||||
ifndef GOPKG
|
||||
ifneq ($(wildcard go.mod),)
|
||||
GOPKG = $(shell sed '/module/!d;s/^omdule\ //' go.mod)
|
||||
endif
|
||||
endif
|
||||
ifdef GOPKG
|
||||
GO ?= go
|
||||
GOPATH ?= $(HOME)/go
|
||||
GO_INSTALL_OPTS ?=
|
||||
GO_TEST_OPTS ?= -test.timeout=30s
|
||||
GOMOD_DIRS ?= $(sort $(call novendor,$(dir $(call rwildcard,*,*/go.mod go.mod))))
|
||||
GOCOVERAGE_FILE ?= ./coverage.txt
|
||||
GOTESTJSON_FILE ?= ./go-test.json
|
||||
GOBUILDLOG_FILE ?= ./go-build.log
|
||||
GOINSTALLLOG_FILE ?= ./go-install.log
|
||||
|
||||
ifdef GOBINS
|
||||
.PHONY: go.install
|
||||
go.install:
|
||||
ifeq ($(CI),true)
|
||||
@rm -f /tmp/goinstall.log
|
||||
@set -e; for dir in $(GOBINS); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) install -v $(GO_INSTALL_OPTS) .; \
|
||||
); done 2>&1 | tee $(GOINSTALLLOG_FILE)
|
||||
|
||||
else
|
||||
@set -e; for dir in $(GOBINS); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) install $(GO_INSTALL_OPTS) .; \
|
||||
); done
|
||||
endif
|
||||
INSTALL_STEPS += go.install
|
||||
|
||||
.PHONY: go.release
|
||||
go.release:
|
||||
$(call check-program, goreleaser)
|
||||
goreleaser --snapshot --skip-publish --rm-dist
|
||||
@echo -n "Do you want to release? [y/N] " && read ans && \
|
||||
if [ $${ans:-N} = y ]; then set -xe; goreleaser --rm-dist; fi
|
||||
RELEASE_STEPS += go.release
|
||||
endif
|
||||
|
||||
.PHONY: go.unittest
|
||||
go.unittest:
|
||||
ifeq ($(CI),true)
|
||||
@echo "mode: atomic" > /tmp/gocoverage
|
||||
@rm -f $(GOTESTJSON_FILE)
|
||||
@set -e; for dir in $(GOMOD_DIRS); do (set -e; (set -euf pipefail; \
|
||||
cd $$dir; \
|
||||
(($(GO) test ./... $(GO_TEST_OPTS) -cover -coverprofile=/tmp/profile.out -covermode=atomic -race -json && touch $@.ok) | tee -a $(GOTESTJSON_FILE) 3>&1 1>&2 2>&3 | tee -a $(GOBUILDLOG_FILE); \
|
||||
); \
|
||||
rm $@.ok 2>/dev/null || exit 1; \
|
||||
if [ -f /tmp/profile.out ]; then \
|
||||
cat /tmp/profile.out | sed "/mode: atomic/d" >> /tmp/gocoverage; \
|
||||
rm -f /tmp/profile.out; \
|
||||
fi)); done
|
||||
@mv /tmp/gocoverage $(GOCOVERAGE_FILE)
|
||||
else
|
||||
@echo "mode: atomic" > /tmp/gocoverage
|
||||
@set -e; for dir in $(GOMOD_DIRS); do (set -e; (set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) test ./... $(GO_TEST_OPTS) -cover -coverprofile=/tmp/profile.out -covermode=atomic -race); \
|
||||
if [ -f /tmp/profile.out ]; then \
|
||||
cat /tmp/profile.out | sed "/mode: atomic/d" >> /tmp/gocoverage; \
|
||||
rm -f /tmp/profile.out; \
|
||||
fi); done
|
||||
@mv /tmp/gocoverage $(GOCOVERAGE_FILE)
|
||||
endif
|
||||
|
||||
.PHONY: go.checkdoc
|
||||
go.checkdoc:
|
||||
go doc $(first $(GOMOD_DIRS))
|
||||
|
||||
.PHONY: go.coverfunc
|
||||
go.coverfunc: go.unittest
|
||||
go tool cover -func=$(GOCOVERAGE_FILE) | grep -v .pb.go: | grep -v .pb.gw.go:
|
||||
|
||||
.PHONY: go.lint
|
||||
go.lint:
|
||||
@set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
golangci-lint run --verbose ./...; \
|
||||
); done
|
||||
|
||||
.PHONY: go.tidy
|
||||
go.tidy:
|
||||
@# tidy dirs with go.mod files
|
||||
@set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) mod tidy; \
|
||||
); done
|
||||
|
||||
.PHONY: go.depaware-update
|
||||
go.depaware-update: go.tidy
|
||||
@# gen depaware for bins
|
||||
@set -e; for dir in $(GOBINS); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) run github.com/tailscale/depaware --update .; \
|
||||
); done
|
||||
@# tidy unused depaware deps if not in a tools_test.go file
|
||||
@set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) mod tidy; \
|
||||
); done
|
||||
|
||||
.PHONY: go.depaware-check
|
||||
go.depaware-check: go.tidy
|
||||
@# gen depaware for bins
|
||||
@set -e; for dir in $(GOBINS); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) run github.com/tailscale/depaware --check .; \
|
||||
); done
|
||||
|
||||
|
||||
.PHONY: go.build
|
||||
go.build:
|
||||
@set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) build ./...; \
|
||||
); done
|
||||
|
||||
.PHONY: go.bump-deps
|
||||
go.bumpdeps:
|
||||
@set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) get -u ./...; \
|
||||
); done
|
||||
|
||||
.PHONY: go.bump-deps
|
||||
go.fmt:
|
||||
@set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) run golang.org/x/tools/cmd/goimports -w `go list -f '{{.Dir}}' ./...` \
|
||||
); done
|
||||
|
||||
VERIFY_STEPS += go.depaware-check
|
||||
BUILD_STEPS += go.build
|
||||
BUMPDEPS_STEPS += go.bumpdeps go.depaware-update
|
||||
TIDY_STEPS += go.tidy
|
||||
LINT_STEPS += go.lint
|
||||
UNITTEST_STEPS += go.unittest
|
||||
FMT_STEPS += go.fmt
|
||||
|
||||
# FIXME: disabled, because currently slow
|
||||
# new rule that is manually run sometimes, i.e. `make pre-release` or `make maintenance`.
|
||||
# alternative: run it each time the go.mod is changed
|
||||
#GENERATE_STEPS += go.depaware-update
|
||||
endif
|
||||
|
||||
##
|
||||
## Gitattributes
|
||||
##
|
||||
|
||||
ifneq ($(wildcard .gitattributes),)
|
||||
.PHONY: _linguist-ignored
|
||||
_linguist-kept:
|
||||
@git check-attr linguist-vendored $(shell git check-attr linguist-generated $(shell find . -type f | grep -v .git/) | grep unspecified | cut -d: -f1) | grep unspecified | cut -d: -f1 | sort
|
||||
|
||||
.PHONY: _linguist-kept
|
||||
_linguist-ignored:
|
||||
@git check-attr linguist-vendored linguist-ignored `find . -not -path './.git/*' -type f` | grep '\ set$$' | cut -d: -f1 | sort -u
|
||||
endif
|
||||
|
||||
##
|
||||
## Node
|
||||
##
|
||||
|
||||
ifndef NPM_PACKAGES
|
||||
ifneq ($(wildcard package.json),)
|
||||
NPM_PACKAGES = .
|
||||
endif
|
||||
endif
|
||||
ifdef NPM_PACKAGES
|
||||
.PHONY: npm.publish
|
||||
npm.publish:
|
||||
@echo -n "Do you want to npm publish? [y/N] " && read ans && \
|
||||
@if [ $${ans:-N} = y ]; then \
|
||||
set -e; for dir in $(NPM_PACKAGES); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
npm publish --access=public; \
|
||||
); done; \
|
||||
fi
|
||||
RELEASE_STEPS += npm.publish
|
||||
endif
|
||||
|
||||
##
|
||||
## Docker
|
||||
##
|
||||
|
||||
docker_build = docker build \
|
||||
--build-arg VCS_REF=`git rev-parse --short HEAD` \
|
||||
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
|
||||
--build-arg VERSION=`git describe --tags --always` \
|
||||
-t "$2" -f "$1" "$(dir $1)"
|
||||
|
||||
ifndef DOCKERFILE_PATH
|
||||
DOCKERFILE_PATH = ./Dockerfile
|
||||
endif
|
||||
ifndef DOCKER_IMAGE
|
||||
ifneq ($(wildcard Dockerfile),)
|
||||
DOCKER_IMAGE = $(notdir $(PWD))
|
||||
endif
|
||||
endif
|
||||
ifdef DOCKER_IMAGE
|
||||
ifneq ($(DOCKER_IMAGE),none)
|
||||
.PHONY: docker.build
|
||||
docker.build:
|
||||
$(call check-program, docker)
|
||||
$(call docker_build,$(DOCKERFILE_PATH),$(DOCKER_IMAGE))
|
||||
|
||||
BUILD_STEPS += docker.build
|
||||
endif
|
||||
endif
|
||||
|
||||
##
|
||||
## Common
|
||||
##
|
||||
|
||||
TEST_STEPS += $(UNITTEST_STEPS)
|
||||
TEST_STEPS += $(LINT_STEPS)
|
||||
TEST_STEPS += $(TIDY_STEPS)
|
||||
|
||||
ifneq ($(strip $(TEST_STEPS)),)
|
||||
.PHONY: test
|
||||
test: $(PRE_TEST_STEPS) $(TEST_STEPS)
|
||||
endif
|
||||
|
||||
ifdef INSTALL_STEPS
|
||||
.PHONY: install
|
||||
install: $(PRE_INSTALL_STEPS) $(INSTALL_STEPS)
|
||||
endif
|
||||
|
||||
ifdef UNITTEST_STEPS
|
||||
.PHONY: unittest
|
||||
unittest: $(PRE_UNITTEST_STEPS) $(UNITTEST_STEPS)
|
||||
endif
|
||||
|
||||
ifdef LINT_STEPS
|
||||
.PHONY: lint
|
||||
lint: $(PRE_LINT_STEPS) $(FMT_STEPS) $(LINT_STEPS)
|
||||
endif
|
||||
|
||||
ifdef TIDY_STEPS
|
||||
.PHONY: tidy
|
||||
tidy: $(PRE_TIDY_STEPS) $(TIDY_STEPS)
|
||||
endif
|
||||
|
||||
ifdef BUILD_STEPS
|
||||
.PHONY: build
|
||||
build: $(PRE_BUILD_STEPS) $(BUILD_STEPS)
|
||||
endif
|
||||
|
||||
ifdef VERIFY_STEPS
|
||||
.PHONY: verify
|
||||
verify: $(PRE_VERIFY_STEPS) $(VERIFY_STEPS)
|
||||
endif
|
||||
|
||||
ifdef RELEASE_STEPS
|
||||
.PHONY: release
|
||||
release: $(PRE_RELEASE_STEPS) $(RELEASE_STEPS)
|
||||
endif
|
||||
|
||||
ifdef BUMPDEPS_STEPS
|
||||
.PHONY: bumpdeps
|
||||
bumpdeps: $(PRE_BUMDEPS_STEPS) $(BUMPDEPS_STEPS)
|
||||
endif
|
||||
|
||||
ifdef FMT_STEPS
|
||||
.PHONY: fmt
|
||||
fmt: $(PRE_FMT_STEPS) $(FMT_STEPS)
|
||||
endif
|
||||
|
||||
ifdef GENERATE_STEPS
|
||||
.PHONY: generate
|
||||
generate: $(PRE_GENERATE_STEPS) $(GENERATE_STEPS)
|
||||
endif
|
||||
|
||||
.PHONY: help
|
||||
help::
|
||||
@echo "General commands:"
|
||||
@[ "$(BUILD_STEPS)" != "" ] && echo " build" || true
|
||||
@[ "$(BUMPDEPS_STEPS)" != "" ] && echo " bumpdeps" || true
|
||||
@[ "$(FMT_STEPS)" != "" ] && echo " fmt" || true
|
||||
@[ "$(GENERATE_STEPS)" != "" ] && echo " generate" || true
|
||||
@[ "$(INSTALL_STEPS)" != "" ] && echo " install" || true
|
||||
@[ "$(LINT_STEPS)" != "" ] && echo " lint" || true
|
||||
@[ "$(RELEASE_STEPS)" != "" ] && echo " release" || true
|
||||
@[ "$(TEST_STEPS)" != "" ] && echo " test" || true
|
||||
@[ "$(TIDY_STEPS)" != "" ] && echo " tidy" || true
|
||||
@[ "$(UNITTEST_STEPS)" != "" ] && echo " unittest" || true
|
||||
@[ "$(VERIFY_STEPS)" != "" ] && echo " verify" || true
|
||||
@# FIXME: list other commands
|
||||
|
||||
print-% : ; $(info $* is a $(flavor $*) variable set to [$($*)]) @true
|
||||
134
server.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/urfave/cli"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"moul.io/sshportal/pkg/bastion"
|
||||
)
|
||||
|
||||
type serverConfig struct {
|
||||
aesKey string
|
||||
dbDriver, dbURL string
|
||||
logsLocation string
|
||||
bindAddr string
|
||||
debug, demo bool
|
||||
idleTimeout time.Duration
|
||||
}
|
||||
|
||||
func parseServerConfig(c *cli.Context) (*serverConfig, error) {
|
||||
ret := &serverConfig{
|
||||
aesKey: c.String("aes-key"),
|
||||
dbDriver: c.String("db-driver"),
|
||||
dbURL: c.String("db-conn"),
|
||||
bindAddr: c.String("bind-address"),
|
||||
debug: c.Bool("debug"),
|
||||
demo: c.Bool("demo"),
|
||||
logsLocation: c.String("logs-location"),
|
||||
idleTimeout: c.Duration("idle-timeout"),
|
||||
}
|
||||
switch len(ret.aesKey) {
|
||||
case 0, 16, 24, 32:
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid aes key size, should be 16 or 24, 32")
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func ensureLogDirectory(location string) error {
|
||||
// check for the logdir existence
|
||||
logsLocation, err := os.Stat(location)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return os.MkdirAll(location, os.ModeDir|os.FileMode(0750))
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !logsLocation.IsDir() {
|
||||
return fmt.Errorf("log directory cannot be created")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func server(c *serverConfig) (err error) {
|
||||
var db = (*gorm.DB)(nil)
|
||||
|
||||
// try to setup the local DB
|
||||
if db, err = gorm.Open(c.dbDriver, c.dbURL); err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
origErr := err
|
||||
err = db.Close()
|
||||
if origErr != nil {
|
||||
err = origErr
|
||||
}
|
||||
}()
|
||||
if err = db.DB().Ping(); err != nil {
|
||||
return
|
||||
}
|
||||
db.LogMode(c.debug)
|
||||
if err = bastion.DBInit(db); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// create TCP listening socket
|
||||
ln, err := net.Listen("tcp", c.bindAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// configure server
|
||||
srv := &ssh.Server{
|
||||
Addr: c.bindAddr,
|
||||
Handler: func(s ssh.Session) { bastion.ShellHandler(s, GitTag, GitSha, GitTag) }, // ssh.Server.Handler is the handler for the DefaultSessionHandler
|
||||
Version: fmt.Sprintf("sshportal-%s", GitTag),
|
||||
ChannelHandlers: map[string]ssh.ChannelHandler{
|
||||
"default": bastion.ChannelHandler,
|
||||
},
|
||||
}
|
||||
|
||||
// configure channel handler
|
||||
bastion.DefaultChannelHandler = func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
|
||||
switch newChan.ChannelType() {
|
||||
case "session":
|
||||
go ssh.DefaultSessionHandler(srv, conn, newChan, ctx)
|
||||
case "direct-tcpip":
|
||||
go ssh.DirectTCPIPHandler(srv, conn, newChan, ctx)
|
||||
default:
|
||||
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
|
||||
log.Printf("failed to reject chan: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.idleTimeout != 0 {
|
||||
srv.IdleTimeout = c.idleTimeout
|
||||
// gliderlabs/ssh requires MaxTimeout to be non-zero if we want to use IdleTimeout.
|
||||
// So, set it to the max value, because we don't want a max timeout.
|
||||
srv.MaxTimeout = math.MaxInt64
|
||||
}
|
||||
|
||||
for _, opt := range []ssh.Option{
|
||||
// custom PublicKeyAuth handler
|
||||
ssh.PublicKeyAuth(bastion.PublicKeyAuthHandler(db, c.logsLocation, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
|
||||
ssh.PasswordAuth(bastion.PasswordAuthHandler(db, c.logsLocation, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
|
||||
// retrieve sshportal SSH private key from database
|
||||
bastion.PrivateKeyFromDB(db, c.aesKey),
|
||||
} {
|
||||
if err := srv.SetOption(opt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("info: SSH Server accepting connections on %s, idle-timout=%v", c.bindAddr, c.idleTimeout)
|
||||
return srv.Serve(ln)
|
||||
}
|
||||
80
testserver.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/kr/pty"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// testServer is an hidden handler used for integration tests
|
||||
func testServer(c *cli.Context) error {
|
||||
ssh.Handle(func(s ssh.Session) {
|
||||
helloMsg := struct {
|
||||
User string
|
||||
Environ []string
|
||||
Command []string
|
||||
}{
|
||||
User: s.User(),
|
||||
Environ: s.Environ(),
|
||||
Command: s.Command(),
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(s).Encode(&helloMsg); err != nil {
|
||||
log.Fatalf("failed to write helloMsg: %v", err)
|
||||
}
|
||||
cmd := exec.Command(s.Command()[0], s.Command()[1:]...) // #nosec
|
||||
if s.Command() == nil {
|
||||
cmd = exec.Command("/bin/sh") // #nosec
|
||||
}
|
||||
ptyReq, winCh, isPty := s.Pty()
|
||||
var cmdErr error
|
||||
if isPty {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
|
||||
f, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
fmt.Fprintf(s, "failed to run command: %v\n", err) // #nosec
|
||||
_ = s.Exit(1) // #nosec
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
for win := range winCh {
|
||||
_, _, _ = syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ),
|
||||
uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(win.Height), uint16(win.Width), 0, 0}))) // #nosec
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
// stdin
|
||||
_, _ = io.Copy(f, s) // #nosec
|
||||
}()
|
||||
// stdout
|
||||
_, _ = io.Copy(s, f) // #nosec
|
||||
cmdErr = cmd.Wait()
|
||||
} else {
|
||||
//cmd.Stdin = s
|
||||
cmd.Stdout = s
|
||||
cmd.Stderr = s
|
||||
cmdErr = cmd.Run()
|
||||
}
|
||||
|
||||
if cmdErr != nil {
|
||||
if exitError, ok := cmdErr.(*exec.ExitError); ok {
|
||||
_ = s.Exit(exitError.Sys().(syscall.WaitStatus).ExitStatus()) // #nosec
|
||||
return
|
||||
}
|
||||
}
|
||||
_ = s.Exit(cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()) // #nosec
|
||||
})
|
||||
|
||||
log.Println("starting ssh server on port 2222...")
|
||||
return ssh.ListenAndServe(":2222", nil)
|
||||
}
|
||||
14
testserver_unsupported.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// testServer is an hidden handler used for integration tests
|
||||
func testServer(c *cli.Context) error {
|
||||
return fmt.Errorf("not available on windows")
|
||||
}
|
||||
13
util.go
@@ -1,13 +0,0 @@
|
||||
package main
|
||||
|
||||
import "math/rand"
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
func RandStringBytes(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
20
vendor/github.com/anmitsu/go-shlex/LICENSE
generated
vendored
@@ -1,20 +0,0 @@
|
||||
Copyright (c) anmitsu <anmitsu.s@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
38
vendor/github.com/anmitsu/go-shlex/README.md
generated
vendored
@@ -1,38 +0,0 @@
|
||||
# go-shlex
|
||||
|
||||
go-shlex is a library to make a lexical analyzer like Unix shell for
|
||||
Go.
|
||||
|
||||
## Install
|
||||
|
||||
go get -u "github.com/anmitsu/go-shlex"
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/anmitsu/go-shlex"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd := `cp -Rdp "file name" 'file name2' dir\ name`
|
||||
words, err := shlex.Split(cmd, true)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, w := range words {
|
||||
fmt.Println(w)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
http://godoc.org/github.com/anmitsu/go-shlex
|
||||
|
||||
193
vendor/github.com/anmitsu/go-shlex/shlex.go
generated
vendored
@@ -1,193 +0,0 @@
|
||||
// Package shlex provides a simple lexical analysis like Unix shell.
|
||||
package shlex
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoClosing = errors.New("No closing quotation")
|
||||
ErrNoEscaped = errors.New("No escaped character")
|
||||
)
|
||||
|
||||
// Tokenizer is the interface that classifies a token according to
|
||||
// words, whitespaces, quotations, escapes and escaped quotations.
|
||||
type Tokenizer interface {
|
||||
IsWord(rune) bool
|
||||
IsWhitespace(rune) bool
|
||||
IsQuote(rune) bool
|
||||
IsEscape(rune) bool
|
||||
IsEscapedQuote(rune) bool
|
||||
}
|
||||
|
||||
// DefaultTokenizer implements a simple tokenizer like Unix shell.
|
||||
type DefaultTokenizer struct{}
|
||||
|
||||
func (t *DefaultTokenizer) IsWord(r rune) bool {
|
||||
return r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r)
|
||||
}
|
||||
func (t *DefaultTokenizer) IsQuote(r rune) bool {
|
||||
switch r {
|
||||
case '\'', '"':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
func (t *DefaultTokenizer) IsWhitespace(r rune) bool {
|
||||
return unicode.IsSpace(r)
|
||||
}
|
||||
func (t *DefaultTokenizer) IsEscape(r rune) bool {
|
||||
return r == '\\'
|
||||
}
|
||||
func (t *DefaultTokenizer) IsEscapedQuote(r rune) bool {
|
||||
return r == '"'
|
||||
}
|
||||
|
||||
// Lexer represents a lexical analyzer.
|
||||
type Lexer struct {
|
||||
reader *bufio.Reader
|
||||
tokenizer Tokenizer
|
||||
posix bool
|
||||
whitespacesplit bool
|
||||
}
|
||||
|
||||
// NewLexer creates a new Lexer reading from io.Reader. This Lexer
|
||||
// has a DefaultTokenizer according to posix and whitespacesplit
|
||||
// rules.
|
||||
func NewLexer(r io.Reader, posix, whitespacesplit bool) *Lexer {
|
||||
return &Lexer{
|
||||
reader: bufio.NewReader(r),
|
||||
tokenizer: &DefaultTokenizer{},
|
||||
posix: posix,
|
||||
whitespacesplit: whitespacesplit,
|
||||
}
|
||||
}
|
||||
|
||||
// NewLexerString creates a new Lexer reading from a string. This
|
||||
// Lexer has a DefaultTokenizer according to posix and whitespacesplit
|
||||
// rules.
|
||||
func NewLexerString(s string, posix, whitespacesplit bool) *Lexer {
|
||||
return NewLexer(strings.NewReader(s), posix, whitespacesplit)
|
||||
}
|
||||
|
||||
// Split splits a string according to posix or non-posix rules.
|
||||
func Split(s string, posix bool) ([]string, error) {
|
||||
return NewLexerString(s, posix, true).Split()
|
||||
}
|
||||
|
||||
// SetTokenizer sets a Tokenizer.
|
||||
func (l *Lexer) SetTokenizer(t Tokenizer) {
|
||||
l.tokenizer = t
|
||||
}
|
||||
|
||||
func (l *Lexer) Split() ([]string, error) {
|
||||
result := make([]string, 0)
|
||||
for {
|
||||
token, err := l.readToken()
|
||||
if token != "" {
|
||||
result = append(result, token)
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (l *Lexer) readToken() (string, error) {
|
||||
t := l.tokenizer
|
||||
token := ""
|
||||
quoted := false
|
||||
state := ' '
|
||||
escapedstate := ' '
|
||||
scanning:
|
||||
for {
|
||||
next, _, err := l.reader.ReadRune()
|
||||
if err != nil {
|
||||
if t.IsQuote(state) {
|
||||
return token, ErrNoClosing
|
||||
} else if t.IsEscape(state) {
|
||||
return token, ErrNoEscaped
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case t.IsWhitespace(state):
|
||||
switch {
|
||||
case t.IsWhitespace(next):
|
||||
break scanning
|
||||
case l.posix && t.IsEscape(next):
|
||||
escapedstate = 'a'
|
||||
state = next
|
||||
case t.IsWord(next):
|
||||
token += string(next)
|
||||
state = 'a'
|
||||
case t.IsQuote(next):
|
||||
if !l.posix {
|
||||
token += string(next)
|
||||
}
|
||||
state = next
|
||||
default:
|
||||
token = string(next)
|
||||
if l.whitespacesplit {
|
||||
state = 'a'
|
||||
} else if token != "" || (l.posix && quoted) {
|
||||
break scanning
|
||||
}
|
||||
}
|
||||
case t.IsQuote(state):
|
||||
quoted = true
|
||||
switch {
|
||||
case next == state:
|
||||
if !l.posix {
|
||||
token += string(next)
|
||||
break scanning
|
||||
} else {
|
||||
state = 'a'
|
||||
}
|
||||
case l.posix && t.IsEscape(next) && t.IsEscapedQuote(state):
|
||||
escapedstate = state
|
||||
state = next
|
||||
default:
|
||||
token += string(next)
|
||||
}
|
||||
case t.IsEscape(state):
|
||||
if t.IsQuote(escapedstate) && next != state && next != escapedstate {
|
||||
token += string(state)
|
||||
}
|
||||
token += string(next)
|
||||
state = escapedstate
|
||||
case t.IsWord(state):
|
||||
switch {
|
||||
case t.IsWhitespace(next):
|
||||
if token != "" || (l.posix && quoted) {
|
||||
break scanning
|
||||
}
|
||||
case l.posix && t.IsQuote(next):
|
||||
state = next
|
||||
case l.posix && t.IsEscape(next):
|
||||
escapedstate = 'a'
|
||||
state = next
|
||||
case t.IsWord(next) || t.IsQuote(next):
|
||||
token += string(next)
|
||||
default:
|
||||
if l.whitespacesplit {
|
||||
token += string(next)
|
||||
} else if token != "" {
|
||||
l.reader.UnreadRune()
|
||||
break scanning
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
21
vendor/github.com/asaskevich/govalidator/LICENSE
generated
vendored
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Alex Saskevich
|
||||
|
||||
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.
|
||||
410
vendor/github.com/asaskevich/govalidator/README.md
generated
vendored
@@ -1,410 +0,0 @@
|
||||
govalidator
|
||||
===========
|
||||
[](https://gitter.im/asaskevich/govalidator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://godoc.org/github.com/asaskevich/govalidator) [](https://coveralls.io/r/asaskevich/govalidator?branch=master) [](https://app.wercker.com/project/bykey/1ec990b09ea86c910d5f08b0e02c6043)
|
||||
[](https://travis-ci.org/asaskevich/govalidator)
|
||||
|
||||
A package of validators and sanitizers for strings, structs and collections. Based on [validator.js](https://github.com/chriso/validator.js).
|
||||
|
||||
#### Installation
|
||||
Make sure that Go is installed on your computer.
|
||||
Type the following command in your terminal:
|
||||
|
||||
go get github.com/asaskevich/govalidator
|
||||
|
||||
or you can get specified release of the package with `gopkg.in`:
|
||||
|
||||
go get gopkg.in/asaskevich/govalidator.v4
|
||||
|
||||
After it the package is ready to use.
|
||||
|
||||
|
||||
#### Import package in your project
|
||||
Add following line in your `*.go` file:
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
```
|
||||
If you are unhappy to use long `govalidator`, you can do something like this:
|
||||
```go
|
||||
import (
|
||||
valid "github.com/asaskevich/govalidator"
|
||||
)
|
||||
```
|
||||
|
||||
#### Activate behavior to require all fields have a validation tag by default
|
||||
`SetFieldsRequiredByDefault` causes validation to fail when struct fields do not include validations or are not explicitly marked as exempt (using `valid:"-"` or `valid:"email,optional"`). A good place to activate this is a package init function or the main() function.
|
||||
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
|
||||
func init() {
|
||||
govalidator.SetFieldsRequiredByDefault(true)
|
||||
}
|
||||
```
|
||||
|
||||
Here's some code to explain it:
|
||||
```go
|
||||
// this struct definition will fail govalidator.ValidateStruct() (and the field values do not matter):
|
||||
type exampleStruct struct {
|
||||
Name string ``
|
||||
Email string `valid:"email"`
|
||||
}
|
||||
|
||||
// this, however, will only fail when Email is empty or an invalid email address:
|
||||
type exampleStruct2 struct {
|
||||
Name string `valid:"-"`
|
||||
Email string `valid:"email"`
|
||||
}
|
||||
|
||||
// lastly, this will only fail when Email is an invalid email address but not when it's empty:
|
||||
type exampleStruct2 struct {
|
||||
Name string `valid:"-"`
|
||||
Email string `valid:"email,optional"`
|
||||
}
|
||||
```
|
||||
|
||||
#### Recent breaking changes (see [#123](https://github.com/asaskevich/govalidator/pull/123))
|
||||
##### Custom validator function signature
|
||||
A context was added as the second parameter, for structs this is the object being validated – this makes dependent validation possible.
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
|
||||
// old signature
|
||||
func(i interface{}) bool
|
||||
|
||||
// new signature
|
||||
func(i interface{}, o interface{}) bool
|
||||
```
|
||||
|
||||
##### Adding a custom validator
|
||||
This was changed to prevent data races when accessing custom validators.
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
|
||||
// before
|
||||
govalidator.CustomTypeTagMap["customByteArrayValidator"] = CustomTypeValidator(func(i interface{}, o interface{}) bool {
|
||||
// ...
|
||||
})
|
||||
|
||||
// after
|
||||
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", CustomTypeValidator(func(i interface{}, o interface{}) bool {
|
||||
// ...
|
||||
}))
|
||||
```
|
||||
|
||||
#### List of functions:
|
||||
```go
|
||||
func Abs(value float64) float64
|
||||
func BlackList(str, chars string) string
|
||||
func ByteLength(str string, params ...string) bool
|
||||
func StringLength(str string, params ...string) bool
|
||||
func StringMatches(s string, params ...string) bool
|
||||
func CamelCaseToUnderscore(str string) string
|
||||
func Contains(str, substring string) bool
|
||||
func Count(array []interface{}, iterator ConditionIterator) int
|
||||
func Each(array []interface{}, iterator Iterator)
|
||||
func ErrorByField(e error, field string) string
|
||||
func Filter(array []interface{}, iterator ConditionIterator) []interface{}
|
||||
func Find(array []interface{}, iterator ConditionIterator) interface{}
|
||||
func GetLine(s string, index int) (string, error)
|
||||
func GetLines(s string) []string
|
||||
func IsHost(s string) bool
|
||||
func InRange(value, left, right float64) bool
|
||||
func IsASCII(str string) bool
|
||||
func IsAlpha(str string) bool
|
||||
func IsAlphanumeric(str string) bool
|
||||
func IsBase64(str string) bool
|
||||
func IsByteLength(str string, min, max int) bool
|
||||
func IsCreditCard(str string) bool
|
||||
func IsDataURI(str string) bool
|
||||
func IsDialString(str string) bool
|
||||
func IsDNSName(str string) bool
|
||||
func IsDivisibleBy(str, num string) bool
|
||||
func IsEmail(str string) bool
|
||||
func IsFilePath(str string) (bool, int)
|
||||
func IsFloat(str string) bool
|
||||
func IsFullWidth(str string) bool
|
||||
func IsHalfWidth(str string) bool
|
||||
func IsHexadecimal(str string) bool
|
||||
func IsHexcolor(str string) bool
|
||||
func IsIP(str string) bool
|
||||
func IsIPv4(str string) bool
|
||||
func IsIPv6(str string) bool
|
||||
func IsISBN(str string, version int) bool
|
||||
func IsISBN10(str string) bool
|
||||
func IsISBN13(str string) bool
|
||||
func IsISO3166Alpha2(str string) bool
|
||||
func IsISO3166Alpha3(str string) bool
|
||||
func IsInt(str string) bool
|
||||
func IsIn(str string, params ...string) bool
|
||||
func IsJSON(str string) bool
|
||||
func IsLatitude(str string) bool
|
||||
func IsLongitude(str string) bool
|
||||
func IsLowerCase(str string) bool
|
||||
func IsMAC(str string) bool
|
||||
func IsMongoID(str string) bool
|
||||
func IsMultibyte(str string) bool
|
||||
func IsNatural(value float64) bool
|
||||
func IsNegative(value float64) bool
|
||||
func IsNonNegative(value float64) bool
|
||||
func IsNonPositive(value float64) bool
|
||||
func IsNull(str string) bool
|
||||
func IsNumeric(str string) bool
|
||||
func IsPort(str string) bool
|
||||
func IsPositive(value float64) bool
|
||||
func IsPrintableASCII(str string) bool
|
||||
func IsRGBcolor(str string) bool
|
||||
func IsRequestURI(rawurl string) bool
|
||||
func IsRequestURL(rawurl string) bool
|
||||
func IsSSN(str string) bool
|
||||
func IsSemver(str string) bool
|
||||
func IsURL(str string) bool
|
||||
func IsUTFDigit(str string) bool
|
||||
func IsUTFLetter(str string) bool
|
||||
func IsUTFLetterNumeric(str string) bool
|
||||
func IsUTFNumeric(str string) bool
|
||||
func IsUUID(str string) bool
|
||||
func IsUUIDv3(str string) bool
|
||||
func IsUUIDv4(str string) bool
|
||||
func IsUUIDv5(str string) bool
|
||||
func IsUpperCase(str string) bool
|
||||
func IsVariableWidth(str string) bool
|
||||
func IsWhole(value float64) bool
|
||||
func LeftTrim(str, chars string) string
|
||||
func Map(array []interface{}, iterator ResultIterator) []interface{}
|
||||
func Matches(str, pattern string) bool
|
||||
func NormalizeEmail(str string) (string, error)
|
||||
func PadBoth(str string, padStr string, padLen int) string
|
||||
func PadLeft(str string, padStr string, padLen int) string
|
||||
func PadRight(str string, padStr string, padLen int) string
|
||||
func RemoveTags(s string) string
|
||||
func ReplacePattern(str, pattern, replace string) string
|
||||
func Reverse(s string) string
|
||||
func RightTrim(str, chars string) string
|
||||
func SafeFileName(str string) string
|
||||
func Sign(value float64) float64
|
||||
func StripLow(str string, keepNewLines bool) string
|
||||
func ToBoolean(str string) (bool, error)
|
||||
func ToFloat(str string) (float64, error)
|
||||
func ToInt(str string) (int64, error)
|
||||
func ToJSON(obj interface{}) (string, error)
|
||||
func ToString(obj interface{}) string
|
||||
func Trim(str, chars string) string
|
||||
func Truncate(str string, length int, ending string) string
|
||||
func UnderscoreToCamelCase(s string) string
|
||||
func ValidateStruct(s interface{}) (bool, error)
|
||||
func WhiteList(str, chars string) string
|
||||
type ConditionIterator
|
||||
type Error
|
||||
func (e Error) Error() string
|
||||
type Errors
|
||||
func (es Errors) Error() string
|
||||
type ISO3166Entry
|
||||
type Iterator
|
||||
type ParamValidator
|
||||
type ResultIterator
|
||||
type UnsupportedTypeError
|
||||
func (e *UnsupportedTypeError) Error() string
|
||||
type Validator
|
||||
```
|
||||
|
||||
#### Examples
|
||||
###### IsURL
|
||||
```go
|
||||
println(govalidator.IsURL(`http://user@pass:domain.com/path/page`))
|
||||
```
|
||||
###### ToString
|
||||
```go
|
||||
type User struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
str := govalidator.ToString(&User{"John", "Juan"})
|
||||
println(str)
|
||||
```
|
||||
###### Each, Map, Filter, Count for slices
|
||||
Each iterates over the slice/array and calls Iterator for every item
|
||||
```go
|
||||
data := []interface{}{1, 2, 3, 4, 5}
|
||||
var fn govalidator.Iterator = func(value interface{}, index int) {
|
||||
println(value.(int))
|
||||
}
|
||||
govalidator.Each(data, fn)
|
||||
```
|
||||
```go
|
||||
data := []interface{}{1, 2, 3, 4, 5}
|
||||
var fn govalidator.ResultIterator = func(value interface{}, index int) interface{} {
|
||||
return value.(int) * 3
|
||||
}
|
||||
_ = govalidator.Map(data, fn) // result = []interface{}{1, 6, 9, 12, 15}
|
||||
```
|
||||
```go
|
||||
data := []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
var fn govalidator.ConditionIterator = func(value interface{}, index int) bool {
|
||||
return value.(int)%2 == 0
|
||||
}
|
||||
_ = govalidator.Filter(data, fn) // result = []interface{}{2, 4, 6, 8, 10}
|
||||
_ = govalidator.Count(data, fn) // result = 5
|
||||
```
|
||||
###### ValidateStruct [#2](https://github.com/asaskevich/govalidator/pull/2)
|
||||
If you want to validate structs, you can use tag `valid` for any field in your structure. All validators used with this field in one tag are separated by comma. If you want to skip validation, place `-` in your tag. If you need a validator that is not on the list below, you can add it like this:
|
||||
```go
|
||||
govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
|
||||
return str == "duck"
|
||||
})
|
||||
```
|
||||
For completely custom validators (interface-based), see below.
|
||||
|
||||
Here is a list of available validators for struct fields (validator - used function):
|
||||
```go
|
||||
"email": IsEmail,
|
||||
"url": IsURL,
|
||||
"dialstring": IsDialString,
|
||||
"requrl": IsRequestURL,
|
||||
"requri": IsRequestURI,
|
||||
"alpha": IsAlpha,
|
||||
"utfletter": IsUTFLetter,
|
||||
"alphanum": IsAlphanumeric,
|
||||
"utfletternum": IsUTFLetterNumeric,
|
||||
"numeric": IsNumeric,
|
||||
"utfnumeric": IsUTFNumeric,
|
||||
"utfdigit": IsUTFDigit,
|
||||
"hexadecimal": IsHexadecimal,
|
||||
"hexcolor": IsHexcolor,
|
||||
"rgbcolor": IsRGBcolor,
|
||||
"lowercase": IsLowerCase,
|
||||
"uppercase": IsUpperCase,
|
||||
"int": IsInt,
|
||||
"float": IsFloat,
|
||||
"null": IsNull,
|
||||
"uuid": IsUUID,
|
||||
"uuidv3": IsUUIDv3,
|
||||
"uuidv4": IsUUIDv4,
|
||||
"uuidv5": IsUUIDv5,
|
||||
"creditcard": IsCreditCard,
|
||||
"isbn10": IsISBN10,
|
||||
"isbn13": IsISBN13,
|
||||
"json": IsJSON,
|
||||
"multibyte": IsMultibyte,
|
||||
"ascii": IsASCII,
|
||||
"printableascii": IsPrintableASCII,
|
||||
"fullwidth": IsFullWidth,
|
||||
"halfwidth": IsHalfWidth,
|
||||
"variablewidth": IsVariableWidth,
|
||||
"base64": IsBase64,
|
||||
"datauri": IsDataURI,
|
||||
"ip": IsIP,
|
||||
"port": IsPort,
|
||||
"ipv4": IsIPv4,
|
||||
"ipv6": IsIPv6,
|
||||
"dns": IsDNSName,
|
||||
"host": IsHost,
|
||||
"mac": IsMAC,
|
||||
"latitude": IsLatitude,
|
||||
"longitude": IsLongitude,
|
||||
"ssn": IsSSN,
|
||||
"semver": IsSemver,
|
||||
"rfc3339": IsRFC3339,
|
||||
"ISO3166Alpha2": IsISO3166Alpha2,
|
||||
"ISO3166Alpha3": IsISO3166Alpha3,
|
||||
```
|
||||
Validators with parameters
|
||||
|
||||
```go
|
||||
"length(min|max)": ByteLength,
|
||||
"runelength(min|max)": RuneLegth,
|
||||
"matches(pattern)": StringMatches,
|
||||
"in(string1|string2|...|stringN)": IsIn,
|
||||
```
|
||||
|
||||
And here is small example of usage:
|
||||
```go
|
||||
type Post struct {
|
||||
Title string `valid:"alphanum,required"`
|
||||
Message string `valid:"duck,ascii"`
|
||||
AuthorIP string `valid:"ipv4"`
|
||||
Date string `valid:"-"`
|
||||
}
|
||||
post := &Post{
|
||||
Title: "My Example Post",
|
||||
Message: "duck",
|
||||
AuthorIP: "123.234.54.3",
|
||||
}
|
||||
|
||||
// Add your own struct validation tags
|
||||
govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
|
||||
return str == "duck"
|
||||
})
|
||||
|
||||
result, err := govalidator.ValidateStruct(post)
|
||||
if err != nil {
|
||||
println("error: " + err.Error())
|
||||
}
|
||||
println(result)
|
||||
```
|
||||
###### WhiteList
|
||||
```go
|
||||
// Remove all characters from string ignoring characters between "a" and "z"
|
||||
println(govalidator.WhiteList("a3a43a5a4a3a2a23a4a5a4a3a4", "a-z") == "aaaaaaaaaaaa")
|
||||
```
|
||||
|
||||
###### Custom validation functions
|
||||
Custom validation using your own domain specific validators is also available - here's an example of how to use it:
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
|
||||
type CustomByteArray [6]byte // custom types are supported and can be validated
|
||||
|
||||
type StructWithCustomByteArray struct {
|
||||
ID CustomByteArray `valid:"customByteArrayValidator,customMinLengthValidator"` // multiple custom validators are possible as well and will be evaluated in sequence
|
||||
Email string `valid:"email"`
|
||||
CustomMinLength int `valid:"-"`
|
||||
}
|
||||
|
||||
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||
switch v := context.(type) { // you can type switch on the context interface being validated
|
||||
case StructWithCustomByteArray:
|
||||
// you can check and validate against some other field in the context,
|
||||
// return early or not validate against the context at all – your choice
|
||||
case SomeOtherType:
|
||||
// ...
|
||||
default:
|
||||
// expecting some other type? Throw/panic here or continue
|
||||
}
|
||||
|
||||
switch v := i.(type) { // type switch on the struct field being validated
|
||||
case CustomByteArray:
|
||||
for _, e := range v { // this validator checks that the byte array is not empty, i.e. not all zeroes
|
||||
if e != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}))
|
||||
govalidator.CustomTypeTagMap.Set("customMinLengthValidator", CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||
switch v := context.(type) { // this validates a field against the value in another field, i.e. dependent validation
|
||||
case StructWithCustomByteArray:
|
||||
return len(v.ID) >= v.CustomMinLength
|
||||
}
|
||||
return false
|
||||
}))
|
||||
```
|
||||
|
||||
#### Notes
|
||||
Documentation is available here: [godoc.org](https://godoc.org/github.com/asaskevich/govalidator).
|
||||
Full information about code coverage is also available here: [govalidator on gocover.io](http://gocover.io/github.com/asaskevich/govalidator).
|
||||
|
||||
#### Support
|
||||
If you do have a contribution for the package feel free to put up a Pull Request or open Issue.
|
||||
|
||||
#### Special thanks to [contributors](https://github.com/asaskevich/govalidator/graphs/contributors)
|
||||
* [Daniel Lohse](https://github.com/annismckenzie)
|
||||
* [Attila Oláh](https://github.com/attilaolah)
|
||||
* [Daniel Korner](https://github.com/Dadie)
|
||||
* [Steven Wilkin](https://github.com/stevenwilkin)
|
||||
* [Deiwin Sarjas](https://github.com/deiwin)
|
||||
* [Noah Shibley](https://github.com/slugmobile)
|
||||
* [Nathan Davies](https://github.com/nathj07)
|
||||
* [Matt Sanford](https://github.com/mzsanford)
|
||||
* [Simon ccl1115](https://github.com/ccl1115)
|
||||
58
vendor/github.com/asaskevich/govalidator/arrays.go
generated
vendored
@@ -1,58 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
// Iterator is the function that accepts element of slice/array and its index
|
||||
type Iterator func(interface{}, int)
|
||||
|
||||
// ResultIterator is the function that accepts element of slice/array and its index and returns any result
|
||||
type ResultIterator func(interface{}, int) interface{}
|
||||
|
||||
// ConditionIterator is the function that accepts element of slice/array and its index and returns boolean
|
||||
type ConditionIterator func(interface{}, int) bool
|
||||
|
||||
// Each iterates over the slice and apply Iterator to every item
|
||||
func Each(array []interface{}, iterator Iterator) {
|
||||
for index, data := range array {
|
||||
iterator(data, index)
|
||||
}
|
||||
}
|
||||
|
||||
// Map iterates over the slice and apply ResultIterator to every item. Returns new slice as a result.
|
||||
func Map(array []interface{}, iterator ResultIterator) []interface{} {
|
||||
var result = make([]interface{}, len(array))
|
||||
for index, data := range array {
|
||||
result[index] = iterator(data, index)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Find iterates over the slice and apply ConditionIterator to every item. Returns first item that meet ConditionIterator or nil otherwise.
|
||||
func Find(array []interface{}, iterator ConditionIterator) interface{} {
|
||||
for index, data := range array {
|
||||
if iterator(data, index) {
|
||||
return data
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter iterates over the slice and apply ConditionIterator to every item. Returns new slice.
|
||||
func Filter(array []interface{}, iterator ConditionIterator) []interface{} {
|
||||
var result = make([]interface{}, 0)
|
||||
for index, data := range array {
|
||||
if iterator(data, index) {
|
||||
result = append(result, data)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Count iterates over the slice and apply ConditionIterator to every item. Returns count of items that meets ConditionIterator.
|
||||
func Count(array []interface{}, iterator ConditionIterator) int {
|
||||
count := 0
|
||||
for index, data := range array {
|
||||
if iterator(data, index) {
|
||||
count = count + 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
49
vendor/github.com/asaskevich/govalidator/converter.go
generated
vendored
@@ -1,49 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ToString convert the input to a string.
|
||||
func ToString(obj interface{}) string {
|
||||
res := fmt.Sprintf("%v", obj)
|
||||
return string(res)
|
||||
}
|
||||
|
||||
// ToJSON convert the input to a valid JSON string
|
||||
func ToJSON(obj interface{}) (string, error) {
|
||||
res, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
res = []byte("")
|
||||
}
|
||||
return string(res), err
|
||||
}
|
||||
|
||||
// ToFloat convert the input string to a float, or 0.0 if the input is not a float.
|
||||
func ToFloat(str string) (float64, error) {
|
||||
res, err := strconv.ParseFloat(str, 64)
|
||||
if err != nil {
|
||||
res = 0.0
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ToInt convert the input string to an integer, or 0 if the input is not an integer.
|
||||
func ToInt(str string) (int64, error) {
|
||||
res, err := strconv.ParseInt(str, 0, 64)
|
||||
if err != nil {
|
||||
res = 0
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ToBoolean convert the input string to a boolean.
|
||||
func ToBoolean(str string) (bool, error) {
|
||||
res, err := strconv.ParseBool(str)
|
||||
if err != nil {
|
||||
res = false
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
31
vendor/github.com/asaskevich/govalidator/error.go
generated
vendored
@@ -1,31 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
// Errors is an array of multiple errors and conforms to the error interface.
|
||||
type Errors []error
|
||||
|
||||
// Errors returns itself.
|
||||
func (es Errors) Errors() []error {
|
||||
return es
|
||||
}
|
||||
|
||||
func (es Errors) Error() string {
|
||||
var err string
|
||||
for _, e := range es {
|
||||
err += e.Error() + ";"
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Error encapsulates a name, an error and whether there's a custom error message or not.
|
||||
type Error struct {
|
||||
Name string
|
||||
Err error
|
||||
CustomErrorMessageExists bool
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
if e.CustomErrorMessageExists {
|
||||
return e.Err.Error()
|
||||
}
|
||||
return e.Name + ": " + e.Err.Error()
|
||||
}
|
||||
57
vendor/github.com/asaskevich/govalidator/numerics.go
generated
vendored
@@ -1,57 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
import "math"
|
||||
|
||||
// Abs returns absolute value of number
|
||||
func Abs(value float64) float64 {
|
||||
return value * Sign(value)
|
||||
}
|
||||
|
||||
// Sign returns signum of number: 1 in case of value > 0, -1 in case of value < 0, 0 otherwise
|
||||
func Sign(value float64) float64 {
|
||||
if value > 0 {
|
||||
return 1
|
||||
} else if value < 0 {
|
||||
return -1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// IsNegative returns true if value < 0
|
||||
func IsNegative(value float64) bool {
|
||||
return value < 0
|
||||
}
|
||||
|
||||
// IsPositive returns true if value > 0
|
||||
func IsPositive(value float64) bool {
|
||||
return value > 0
|
||||
}
|
||||
|
||||
// IsNonNegative returns true if value >= 0
|
||||
func IsNonNegative(value float64) bool {
|
||||
return value >= 0
|
||||
}
|
||||
|
||||
// IsNonPositive returns true if value <= 0
|
||||
func IsNonPositive(value float64) bool {
|
||||
return value <= 0
|
||||
}
|
||||
|
||||
// InRange returns true if value lies between left and right border
|
||||
func InRange(value, left, right float64) bool {
|
||||
if left > right {
|
||||
left, right = right, left
|
||||
}
|
||||
return value >= left && value <= right
|
||||
}
|
||||
|
||||
// IsWhole returns true if value is whole number
|
||||
func IsWhole(value float64) bool {
|
||||
return Abs(math.Remainder(value, 1)) == 0
|
||||
}
|
||||
|
||||
// IsNatural returns true if value is natural number (positive and whole)
|
||||
func IsNatural(value float64) bool {
|
||||
return IsWhole(value) && IsPositive(value)
|
||||
}
|
||||
91
vendor/github.com/asaskevich/govalidator/patterns.go
generated
vendored
@@ -1,91 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
import "regexp"
|
||||
|
||||
// Basic regular expressions for validating strings
|
||||
const (
|
||||
Email string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
|
||||
CreditCard string = "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$"
|
||||
ISBN10 string = "^(?:[0-9]{9}X|[0-9]{10})$"
|
||||
ISBN13 string = "^(?:[0-9]{13})$"
|
||||
UUID3 string = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
UUID4 string = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||
UUID5 string = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||
UUID string = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
Alpha string = "^[a-zA-Z]+$"
|
||||
Alphanumeric string = "^[a-zA-Z0-9]+$"
|
||||
Numeric string = "^[-+]?[0-9]+$"
|
||||
Int string = "^(?:[-+]?(?:0|[1-9][0-9]*))$"
|
||||
Float string = "^(?:[-+]?(?:[0-9]+))?(?:\\.[0-9]*)?(?:[eE][\\+\\-]?(?:[0-9]+))?$"
|
||||
Hexadecimal string = "^[0-9a-fA-F]+$"
|
||||
Hexcolor string = "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
|
||||
RGBcolor string = "^rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*\\)$"
|
||||
ASCII string = "^[\x00-\x7F]+$"
|
||||
Multibyte string = "[^\x00-\x7F]"
|
||||
FullWidth string = "[^\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
|
||||
HalfWidth string = "[\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
|
||||
Base64 string = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
|
||||
PrintableASCII string = "^[\x20-\x7E]+$"
|
||||
DataURI string = "^data:.+\\/(.+);base64$"
|
||||
Latitude string = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$"
|
||||
Longitude string = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$"
|
||||
DNSName string = `^([a-zA-Z0-9]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9]{1}[a-zA-Z0-9_-]{1,62})*$`
|
||||
IP string = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
|
||||
URLSchema string = `((ftp|tcp|udp|wss?|https?):\/\/)`
|
||||
URLUsername string = `(\S+(:\S*)?@)`
|
||||
Hostname string = ``
|
||||
URLPath string = `((\/|\?|#)[^\s]*)`
|
||||
URLPort string = `(:(\d{1,5}))`
|
||||
URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3])(\.(1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))`
|
||||
URLSubdomain string = `((www\.)|([a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*))`
|
||||
URL string = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))` + URLPort + `?` + URLPath + `?$`
|
||||
SSN string = `^\d{3}[- ]?\d{2}[- ]?\d{4}$`
|
||||
WinPath string = `^[a-zA-Z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$`
|
||||
UnixPath string = `^(/[^/\x00]*)+/?$`
|
||||
Semver string = "^v?(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(-(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\\+[0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*)?$"
|
||||
tagName string = "valid"
|
||||
)
|
||||
|
||||
// Used by IsFilePath func
|
||||
const (
|
||||
// Unknown is unresolved OS type
|
||||
Unknown = iota
|
||||
// Win is Windows type
|
||||
Win
|
||||
// Unix is *nix OS types
|
||||
Unix
|
||||
)
|
||||
|
||||
var (
|
||||
rxEmail = regexp.MustCompile(Email)
|
||||
rxCreditCard = regexp.MustCompile(CreditCard)
|
||||
rxISBN10 = regexp.MustCompile(ISBN10)
|
||||
rxISBN13 = regexp.MustCompile(ISBN13)
|
||||
rxUUID3 = regexp.MustCompile(UUID3)
|
||||
rxUUID4 = regexp.MustCompile(UUID4)
|
||||
rxUUID5 = regexp.MustCompile(UUID5)
|
||||
rxUUID = regexp.MustCompile(UUID)
|
||||
rxAlpha = regexp.MustCompile(Alpha)
|
||||
rxAlphanumeric = regexp.MustCompile(Alphanumeric)
|
||||
rxNumeric = regexp.MustCompile(Numeric)
|
||||
rxInt = regexp.MustCompile(Int)
|
||||
rxFloat = regexp.MustCompile(Float)
|
||||
rxHexadecimal = regexp.MustCompile(Hexadecimal)
|
||||
rxHexcolor = regexp.MustCompile(Hexcolor)
|
||||
rxRGBcolor = regexp.MustCompile(RGBcolor)
|
||||
rxASCII = regexp.MustCompile(ASCII)
|
||||
rxPrintableASCII = regexp.MustCompile(PrintableASCII)
|
||||
rxMultibyte = regexp.MustCompile(Multibyte)
|
||||
rxFullWidth = regexp.MustCompile(FullWidth)
|
||||
rxHalfWidth = regexp.MustCompile(HalfWidth)
|
||||
rxBase64 = regexp.MustCompile(Base64)
|
||||
rxDataURI = regexp.MustCompile(DataURI)
|
||||
rxLatitude = regexp.MustCompile(Latitude)
|
||||
rxLongitude = regexp.MustCompile(Longitude)
|
||||
rxDNSName = regexp.MustCompile(DNSName)
|
||||
rxURL = regexp.MustCompile(URL)
|
||||
rxSSN = regexp.MustCompile(SSN)
|
||||
rxWinPath = regexp.MustCompile(WinPath)
|
||||
rxUnixPath = regexp.MustCompile(UnixPath)
|
||||
rxSemver = regexp.MustCompile(Semver)
|
||||
)
|
||||
385
vendor/github.com/asaskevich/govalidator/types.go
generated
vendored
@@ -1,385 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Validator is a wrapper for a validator function that returns bool and accepts string.
|
||||
type Validator func(str string) bool
|
||||
|
||||
// CustomTypeValidator is a wrapper for validator functions that returns bool and accepts any type.
|
||||
// The second parameter should be the context (in the case of validating a struct: the whole object being validated).
|
||||
type CustomTypeValidator func(i interface{}, o interface{}) bool
|
||||
|
||||
// ParamValidator is a wrapper for validator functions that accepts additional parameters.
|
||||
type ParamValidator func(str string, params ...string) bool
|
||||
type tagOptionsMap map[string]string
|
||||
|
||||
// UnsupportedTypeError is a wrapper for reflect.Type
|
||||
type UnsupportedTypeError struct {
|
||||
Type reflect.Type
|
||||
}
|
||||
|
||||
// stringValues is a slice of reflect.Value holding *reflect.StringValue.
|
||||
// It implements the methods to sort by string.
|
||||
type stringValues []reflect.Value
|
||||
|
||||
// ParamTagMap is a map of functions accept variants parameters
|
||||
var ParamTagMap = map[string]ParamValidator{
|
||||
"length": ByteLength,
|
||||
"runelength": RuneLength,
|
||||
"stringlength": StringLength,
|
||||
"matches": StringMatches,
|
||||
"in": isInRaw,
|
||||
}
|
||||
|
||||
// ParamTagRegexMap maps param tags to their respective regexes.
|
||||
var ParamTagRegexMap = map[string]*regexp.Regexp{
|
||||
"length": regexp.MustCompile("^length\\((\\d+)\\|(\\d+)\\)$"),
|
||||
"runelength": regexp.MustCompile("^runelength\\((\\d+)\\|(\\d+)\\)$"),
|
||||
"stringlength": regexp.MustCompile("^stringlength\\((\\d+)\\|(\\d+)\\)$"),
|
||||
"in": regexp.MustCompile(`^in\((.*)\)`),
|
||||
"matches": regexp.MustCompile(`^matches\((.+)\)$`),
|
||||
}
|
||||
|
||||
type customTypeTagMap struct {
|
||||
validators map[string]CustomTypeValidator
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (tm *customTypeTagMap) Get(name string) (CustomTypeValidator, bool) {
|
||||
tm.RLock()
|
||||
defer tm.RUnlock()
|
||||
v, ok := tm.validators[name]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (tm *customTypeTagMap) Set(name string, ctv CustomTypeValidator) {
|
||||
tm.Lock()
|
||||
defer tm.Unlock()
|
||||
tm.validators[name] = ctv
|
||||
}
|
||||
|
||||
// CustomTypeTagMap is a map of functions that can be used as tags for ValidateStruct function.
|
||||
// Use this to validate compound or custom types that need to be handled as a whole, e.g.
|
||||
// `type UUID [16]byte` (this would be handled as an array of bytes).
|
||||
var CustomTypeTagMap = &customTypeTagMap{validators: make(map[string]CustomTypeValidator)}
|
||||
|
||||
// TagMap is a map of functions, that can be used as tags for ValidateStruct function.
|
||||
var TagMap = map[string]Validator{
|
||||
"email": IsEmail,
|
||||
"url": IsURL,
|
||||
"dialstring": IsDialString,
|
||||
"requrl": IsRequestURL,
|
||||
"requri": IsRequestURI,
|
||||
"alpha": IsAlpha,
|
||||
"utfletter": IsUTFLetter,
|
||||
"alphanum": IsAlphanumeric,
|
||||
"utfletternum": IsUTFLetterNumeric,
|
||||
"numeric": IsNumeric,
|
||||
"utfnumeric": IsUTFNumeric,
|
||||
"utfdigit": IsUTFDigit,
|
||||
"hexadecimal": IsHexadecimal,
|
||||
"hexcolor": IsHexcolor,
|
||||
"rgbcolor": IsRGBcolor,
|
||||
"lowercase": IsLowerCase,
|
||||
"uppercase": IsUpperCase,
|
||||
"int": IsInt,
|
||||
"float": IsFloat,
|
||||
"null": IsNull,
|
||||
"uuid": IsUUID,
|
||||
"uuidv3": IsUUIDv3,
|
||||
"uuidv4": IsUUIDv4,
|
||||
"uuidv5": IsUUIDv5,
|
||||
"creditcard": IsCreditCard,
|
||||
"isbn10": IsISBN10,
|
||||
"isbn13": IsISBN13,
|
||||
"json": IsJSON,
|
||||
"multibyte": IsMultibyte,
|
||||
"ascii": IsASCII,
|
||||
"printableascii": IsPrintableASCII,
|
||||
"fullwidth": IsFullWidth,
|
||||
"halfwidth": IsHalfWidth,
|
||||
"variablewidth": IsVariableWidth,
|
||||
"base64": IsBase64,
|
||||
"datauri": IsDataURI,
|
||||
"ip": IsIP,
|
||||
"port": IsPort,
|
||||
"ipv4": IsIPv4,
|
||||
"ipv6": IsIPv6,
|
||||
"dns": IsDNSName,
|
||||
"host": IsHost,
|
||||
"mac": IsMAC,
|
||||
"latitude": IsLatitude,
|
||||
"longitude": IsLongitude,
|
||||
"ssn": IsSSN,
|
||||
"semver": IsSemver,
|
||||
"rfc3339": IsRFC3339,
|
||||
"ISO3166Alpha2": IsISO3166Alpha2,
|
||||
"ISO3166Alpha3": IsISO3166Alpha3,
|
||||
}
|
||||
|
||||
// ISO3166Entry stores country codes
|
||||
type ISO3166Entry struct {
|
||||
EnglishShortName string
|
||||
FrenchShortName string
|
||||
Alpha2Code string
|
||||
Alpha3Code string
|
||||
Numeric string
|
||||
}
|
||||
|
||||
//ISO3166List based on https://www.iso.org/obp/ui/#search/code/ Code Type "Officially Assigned Codes"
|
||||
var ISO3166List = []ISO3166Entry{
|
||||
{"Afghanistan", "Afghanistan (l')", "AF", "AFG", "004"},
|
||||
{"Albania", "Albanie (l')", "AL", "ALB", "008"},
|
||||
{"Antarctica", "Antarctique (l')", "AQ", "ATA", "010"},
|
||||
{"Algeria", "Algérie (l')", "DZ", "DZA", "012"},
|
||||
{"American Samoa", "Samoa américaines (les)", "AS", "ASM", "016"},
|
||||
{"Andorra", "Andorre (l')", "AD", "AND", "020"},
|
||||
{"Angola", "Angola (l')", "AO", "AGO", "024"},
|
||||
{"Antigua and Barbuda", "Antigua-et-Barbuda", "AG", "ATG", "028"},
|
||||
{"Azerbaijan", "Azerbaïdjan (l')", "AZ", "AZE", "031"},
|
||||
{"Argentina", "Argentine (l')", "AR", "ARG", "032"},
|
||||
{"Australia", "Australie (l')", "AU", "AUS", "036"},
|
||||
{"Austria", "Autriche (l')", "AT", "AUT", "040"},
|
||||
{"Bahamas (the)", "Bahamas (les)", "BS", "BHS", "044"},
|
||||
{"Bahrain", "Bahreïn", "BH", "BHR", "048"},
|
||||
{"Bangladesh", "Bangladesh (le)", "BD", "BGD", "050"},
|
||||
{"Armenia", "Arménie (l')", "AM", "ARM", "051"},
|
||||
{"Barbados", "Barbade (la)", "BB", "BRB", "052"},
|
||||
{"Belgium", "Belgique (la)", "BE", "BEL", "056"},
|
||||
{"Bermuda", "Bermudes (les)", "BM", "BMU", "060"},
|
||||
{"Bhutan", "Bhoutan (le)", "BT", "BTN", "064"},
|
||||
{"Bolivia (Plurinational State of)", "Bolivie (État plurinational de)", "BO", "BOL", "068"},
|
||||
{"Bosnia and Herzegovina", "Bosnie-Herzégovine (la)", "BA", "BIH", "070"},
|
||||
{"Botswana", "Botswana (le)", "BW", "BWA", "072"},
|
||||
{"Bouvet Island", "Bouvet (l'Île)", "BV", "BVT", "074"},
|
||||
{"Brazil", "Brésil (le)", "BR", "BRA", "076"},
|
||||
{"Belize", "Belize (le)", "BZ", "BLZ", "084"},
|
||||
{"British Indian Ocean Territory (the)", "Indien (le Territoire britannique de l'océan)", "IO", "IOT", "086"},
|
||||
{"Solomon Islands", "Salomon (Îles)", "SB", "SLB", "090"},
|
||||
{"Virgin Islands (British)", "Vierges britanniques (les Îles)", "VG", "VGB", "092"},
|
||||
{"Brunei Darussalam", "Brunéi Darussalam (le)", "BN", "BRN", "096"},
|
||||
{"Bulgaria", "Bulgarie (la)", "BG", "BGR", "100"},
|
||||
{"Myanmar", "Myanmar (le)", "MM", "MMR", "104"},
|
||||
{"Burundi", "Burundi (le)", "BI", "BDI", "108"},
|
||||
{"Belarus", "Bélarus (le)", "BY", "BLR", "112"},
|
||||
{"Cambodia", "Cambodge (le)", "KH", "KHM", "116"},
|
||||
{"Cameroon", "Cameroun (le)", "CM", "CMR", "120"},
|
||||
{"Canada", "Canada (le)", "CA", "CAN", "124"},
|
||||
{"Cabo Verde", "Cabo Verde", "CV", "CPV", "132"},
|
||||
{"Cayman Islands (the)", "Caïmans (les Îles)", "KY", "CYM", "136"},
|
||||
{"Central African Republic (the)", "République centrafricaine (la)", "CF", "CAF", "140"},
|
||||
{"Sri Lanka", "Sri Lanka", "LK", "LKA", "144"},
|
||||
{"Chad", "Tchad (le)", "TD", "TCD", "148"},
|
||||
{"Chile", "Chili (le)", "CL", "CHL", "152"},
|
||||
{"China", "Chine (la)", "CN", "CHN", "156"},
|
||||
{"Taiwan (Province of China)", "Taïwan (Province de Chine)", "TW", "TWN", "158"},
|
||||
{"Christmas Island", "Christmas (l'Île)", "CX", "CXR", "162"},
|
||||
{"Cocos (Keeling) Islands (the)", "Cocos (les Îles)/ Keeling (les Îles)", "CC", "CCK", "166"},
|
||||
{"Colombia", "Colombie (la)", "CO", "COL", "170"},
|
||||
{"Comoros (the)", "Comores (les)", "KM", "COM", "174"},
|
||||
{"Mayotte", "Mayotte", "YT", "MYT", "175"},
|
||||
{"Congo (the)", "Congo (le)", "CG", "COG", "178"},
|
||||
{"Congo (the Democratic Republic of the)", "Congo (la République démocratique du)", "CD", "COD", "180"},
|
||||
{"Cook Islands (the)", "Cook (les Îles)", "CK", "COK", "184"},
|
||||
{"Costa Rica", "Costa Rica (le)", "CR", "CRI", "188"},
|
||||
{"Croatia", "Croatie (la)", "HR", "HRV", "191"},
|
||||
{"Cuba", "Cuba", "CU", "CUB", "192"},
|
||||
{"Cyprus", "Chypre", "CY", "CYP", "196"},
|
||||
{"Czech Republic (the)", "tchèque (la République)", "CZ", "CZE", "203"},
|
||||
{"Benin", "Bénin (le)", "BJ", "BEN", "204"},
|
||||
{"Denmark", "Danemark (le)", "DK", "DNK", "208"},
|
||||
{"Dominica", "Dominique (la)", "DM", "DMA", "212"},
|
||||
{"Dominican Republic (the)", "dominicaine (la République)", "DO", "DOM", "214"},
|
||||
{"Ecuador", "Équateur (l')", "EC", "ECU", "218"},
|
||||
{"El Salvador", "El Salvador", "SV", "SLV", "222"},
|
||||
{"Equatorial Guinea", "Guinée équatoriale (la)", "GQ", "GNQ", "226"},
|
||||
{"Ethiopia", "Éthiopie (l')", "ET", "ETH", "231"},
|
||||
{"Eritrea", "Érythrée (l')", "ER", "ERI", "232"},
|
||||
{"Estonia", "Estonie (l')", "EE", "EST", "233"},
|
||||
{"Faroe Islands (the)", "Féroé (les Îles)", "FO", "FRO", "234"},
|
||||
{"Falkland Islands (the) [Malvinas]", "Falkland (les Îles)/Malouines (les Îles)", "FK", "FLK", "238"},
|
||||
{"South Georgia and the South Sandwich Islands", "Géorgie du Sud-et-les Îles Sandwich du Sud (la)", "GS", "SGS", "239"},
|
||||
{"Fiji", "Fidji (les)", "FJ", "FJI", "242"},
|
||||
{"Finland", "Finlande (la)", "FI", "FIN", "246"},
|
||||
{"Åland Islands", "Åland(les Îles)", "AX", "ALA", "248"},
|
||||
{"France", "France (la)", "FR", "FRA", "250"},
|
||||
{"French Guiana", "Guyane française (la )", "GF", "GUF", "254"},
|
||||
{"French Polynesia", "Polynésie française (la)", "PF", "PYF", "258"},
|
||||
{"French Southern Territories (the)", "Terres australes françaises (les)", "TF", "ATF", "260"},
|
||||
{"Djibouti", "Djibouti", "DJ", "DJI", "262"},
|
||||
{"Gabon", "Gabon (le)", "GA", "GAB", "266"},
|
||||
{"Georgia", "Géorgie (la)", "GE", "GEO", "268"},
|
||||
{"Gambia (the)", "Gambie (la)", "GM", "GMB", "270"},
|
||||
{"Palestine, State of", "Palestine, État de", "PS", "PSE", "275"},
|
||||
{"Germany", "Allemagne (l')", "DE", "DEU", "276"},
|
||||
{"Ghana", "Ghana (le)", "GH", "GHA", "288"},
|
||||
{"Gibraltar", "Gibraltar", "GI", "GIB", "292"},
|
||||
{"Kiribati", "Kiribati", "KI", "KIR", "296"},
|
||||
{"Greece", "Grèce (la)", "GR", "GRC", "300"},
|
||||
{"Greenland", "Groenland (le)", "GL", "GRL", "304"},
|
||||
{"Grenada", "Grenade (la)", "GD", "GRD", "308"},
|
||||
{"Guadeloupe", "Guadeloupe (la)", "GP", "GLP", "312"},
|
||||
{"Guam", "Guam", "GU", "GUM", "316"},
|
||||
{"Guatemala", "Guatemala (le)", "GT", "GTM", "320"},
|
||||
{"Guinea", "Guinée (la)", "GN", "GIN", "324"},
|
||||
{"Guyana", "Guyana (le)", "GY", "GUY", "328"},
|
||||
{"Haiti", "Haïti", "HT", "HTI", "332"},
|
||||
{"Heard Island and McDonald Islands", "Heard-et-Îles MacDonald (l'Île)", "HM", "HMD", "334"},
|
||||
{"Holy See (the)", "Saint-Siège (le)", "VA", "VAT", "336"},
|
||||
{"Honduras", "Honduras (le)", "HN", "HND", "340"},
|
||||
{"Hong Kong", "Hong Kong", "HK", "HKG", "344"},
|
||||
{"Hungary", "Hongrie (la)", "HU", "HUN", "348"},
|
||||
{"Iceland", "Islande (l')", "IS", "ISL", "352"},
|
||||
{"India", "Inde (l')", "IN", "IND", "356"},
|
||||
{"Indonesia", "Indonésie (l')", "ID", "IDN", "360"},
|
||||
{"Iran (Islamic Republic of)", "Iran (République Islamique d')", "IR", "IRN", "364"},
|
||||
{"Iraq", "Iraq (l')", "IQ", "IRQ", "368"},
|
||||
{"Ireland", "Irlande (l')", "IE", "IRL", "372"},
|
||||
{"Israel", "Israël", "IL", "ISR", "376"},
|
||||
{"Italy", "Italie (l')", "IT", "ITA", "380"},
|
||||
{"Côte d'Ivoire", "Côte d'Ivoire (la)", "CI", "CIV", "384"},
|
||||
{"Jamaica", "Jamaïque (la)", "JM", "JAM", "388"},
|
||||
{"Japan", "Japon (le)", "JP", "JPN", "392"},
|
||||
{"Kazakhstan", "Kazakhstan (le)", "KZ", "KAZ", "398"},
|
||||
{"Jordan", "Jordanie (la)", "JO", "JOR", "400"},
|
||||
{"Kenya", "Kenya (le)", "KE", "KEN", "404"},
|
||||
{"Korea (the Democratic People's Republic of)", "Corée (la République populaire démocratique de)", "KP", "PRK", "408"},
|
||||
{"Korea (the Republic of)", "Corée (la République de)", "KR", "KOR", "410"},
|
||||
{"Kuwait", "Koweït (le)", "KW", "KWT", "414"},
|
||||
{"Kyrgyzstan", "Kirghizistan (le)", "KG", "KGZ", "417"},
|
||||
{"Lao People's Democratic Republic (the)", "Lao, République démocratique populaire", "LA", "LAO", "418"},
|
||||
{"Lebanon", "Liban (le)", "LB", "LBN", "422"},
|
||||
{"Lesotho", "Lesotho (le)", "LS", "LSO", "426"},
|
||||
{"Latvia", "Lettonie (la)", "LV", "LVA", "428"},
|
||||
{"Liberia", "Libéria (le)", "LR", "LBR", "430"},
|
||||
{"Libya", "Libye (la)", "LY", "LBY", "434"},
|
||||
{"Liechtenstein", "Liechtenstein (le)", "LI", "LIE", "438"},
|
||||
{"Lithuania", "Lituanie (la)", "LT", "LTU", "440"},
|
||||
{"Luxembourg", "Luxembourg (le)", "LU", "LUX", "442"},
|
||||
{"Macao", "Macao", "MO", "MAC", "446"},
|
||||
{"Madagascar", "Madagascar", "MG", "MDG", "450"},
|
||||
{"Malawi", "Malawi (le)", "MW", "MWI", "454"},
|
||||
{"Malaysia", "Malaisie (la)", "MY", "MYS", "458"},
|
||||
{"Maldives", "Maldives (les)", "MV", "MDV", "462"},
|
||||
{"Mali", "Mali (le)", "ML", "MLI", "466"},
|
||||
{"Malta", "Malte", "MT", "MLT", "470"},
|
||||
{"Martinique", "Martinique (la)", "MQ", "MTQ", "474"},
|
||||
{"Mauritania", "Mauritanie (la)", "MR", "MRT", "478"},
|
||||
{"Mauritius", "Maurice", "MU", "MUS", "480"},
|
||||
{"Mexico", "Mexique (le)", "MX", "MEX", "484"},
|
||||
{"Monaco", "Monaco", "MC", "MCO", "492"},
|
||||
{"Mongolia", "Mongolie (la)", "MN", "MNG", "496"},
|
||||
{"Moldova (the Republic of)", "Moldova , République de", "MD", "MDA", "498"},
|
||||
{"Montenegro", "Monténégro (le)", "ME", "MNE", "499"},
|
||||
{"Montserrat", "Montserrat", "MS", "MSR", "500"},
|
||||
{"Morocco", "Maroc (le)", "MA", "MAR", "504"},
|
||||
{"Mozambique", "Mozambique (le)", "MZ", "MOZ", "508"},
|
||||
{"Oman", "Oman", "OM", "OMN", "512"},
|
||||
{"Namibia", "Namibie (la)", "NA", "NAM", "516"},
|
||||
{"Nauru", "Nauru", "NR", "NRU", "520"},
|
||||
{"Nepal", "Népal (le)", "NP", "NPL", "524"},
|
||||
{"Netherlands (the)", "Pays-Bas (les)", "NL", "NLD", "528"},
|
||||
{"Curaçao", "Curaçao", "CW", "CUW", "531"},
|
||||
{"Aruba", "Aruba", "AW", "ABW", "533"},
|
||||
{"Sint Maarten (Dutch part)", "Saint-Martin (partie néerlandaise)", "SX", "SXM", "534"},
|
||||
{"Bonaire, Sint Eustatius and Saba", "Bonaire, Saint-Eustache et Saba", "BQ", "BES", "535"},
|
||||
{"New Caledonia", "Nouvelle-Calédonie (la)", "NC", "NCL", "540"},
|
||||
{"Vanuatu", "Vanuatu (le)", "VU", "VUT", "548"},
|
||||
{"New Zealand", "Nouvelle-Zélande (la)", "NZ", "NZL", "554"},
|
||||
{"Nicaragua", "Nicaragua (le)", "NI", "NIC", "558"},
|
||||
{"Niger (the)", "Niger (le)", "NE", "NER", "562"},
|
||||
{"Nigeria", "Nigéria (le)", "NG", "NGA", "566"},
|
||||
{"Niue", "Niue", "NU", "NIU", "570"},
|
||||
{"Norfolk Island", "Norfolk (l'Île)", "NF", "NFK", "574"},
|
||||
{"Norway", "Norvège (la)", "NO", "NOR", "578"},
|
||||
{"Northern Mariana Islands (the)", "Mariannes du Nord (les Îles)", "MP", "MNP", "580"},
|
||||
{"United States Minor Outlying Islands (the)", "Îles mineures éloignées des États-Unis (les)", "UM", "UMI", "581"},
|
||||
{"Micronesia (Federated States of)", "Micronésie (États fédérés de)", "FM", "FSM", "583"},
|
||||
{"Marshall Islands (the)", "Marshall (Îles)", "MH", "MHL", "584"},
|
||||
{"Palau", "Palaos (les)", "PW", "PLW", "585"},
|
||||
{"Pakistan", "Pakistan (le)", "PK", "PAK", "586"},
|
||||
{"Panama", "Panama (le)", "PA", "PAN", "591"},
|
||||
{"Papua New Guinea", "Papouasie-Nouvelle-Guinée (la)", "PG", "PNG", "598"},
|
||||
{"Paraguay", "Paraguay (le)", "PY", "PRY", "600"},
|
||||
{"Peru", "Pérou (le)", "PE", "PER", "604"},
|
||||
{"Philippines (the)", "Philippines (les)", "PH", "PHL", "608"},
|
||||
{"Pitcairn", "Pitcairn", "PN", "PCN", "612"},
|
||||
{"Poland", "Pologne (la)", "PL", "POL", "616"},
|
||||
{"Portugal", "Portugal (le)", "PT", "PRT", "620"},
|
||||
{"Guinea-Bissau", "Guinée-Bissau (la)", "GW", "GNB", "624"},
|
||||
{"Timor-Leste", "Timor-Leste (le)", "TL", "TLS", "626"},
|
||||
{"Puerto Rico", "Porto Rico", "PR", "PRI", "630"},
|
||||
{"Qatar", "Qatar (le)", "QA", "QAT", "634"},
|
||||
{"Réunion", "Réunion (La)", "RE", "REU", "638"},
|
||||
{"Romania", "Roumanie (la)", "RO", "ROU", "642"},
|
||||
{"Russian Federation (the)", "Russie (la Fédération de)", "RU", "RUS", "643"},
|
||||
{"Rwanda", "Rwanda (le)", "RW", "RWA", "646"},
|
||||
{"Saint Barthélemy", "Saint-Barthélemy", "BL", "BLM", "652"},
|
||||
{"Saint Helena, Ascension and Tristan da Cunha", "Sainte-Hélène, Ascension et Tristan da Cunha", "SH", "SHN", "654"},
|
||||
{"Saint Kitts and Nevis", "Saint-Kitts-et-Nevis", "KN", "KNA", "659"},
|
||||
{"Anguilla", "Anguilla", "AI", "AIA", "660"},
|
||||
{"Saint Lucia", "Sainte-Lucie", "LC", "LCA", "662"},
|
||||
{"Saint Martin (French part)", "Saint-Martin (partie française)", "MF", "MAF", "663"},
|
||||
{"Saint Pierre and Miquelon", "Saint-Pierre-et-Miquelon", "PM", "SPM", "666"},
|
||||
{"Saint Vincent and the Grenadines", "Saint-Vincent-et-les Grenadines", "VC", "VCT", "670"},
|
||||
{"San Marino", "Saint-Marin", "SM", "SMR", "674"},
|
||||
{"Sao Tome and Principe", "Sao Tomé-et-Principe", "ST", "STP", "678"},
|
||||
{"Saudi Arabia", "Arabie saoudite (l')", "SA", "SAU", "682"},
|
||||
{"Senegal", "Sénégal (le)", "SN", "SEN", "686"},
|
||||
{"Serbia", "Serbie (la)", "RS", "SRB", "688"},
|
||||
{"Seychelles", "Seychelles (les)", "SC", "SYC", "690"},
|
||||
{"Sierra Leone", "Sierra Leone (la)", "SL", "SLE", "694"},
|
||||
{"Singapore", "Singapour", "SG", "SGP", "702"},
|
||||
{"Slovakia", "Slovaquie (la)", "SK", "SVK", "703"},
|
||||
{"Viet Nam", "Viet Nam (le)", "VN", "VNM", "704"},
|
||||
{"Slovenia", "Slovénie (la)", "SI", "SVN", "705"},
|
||||
{"Somalia", "Somalie (la)", "SO", "SOM", "706"},
|
||||
{"South Africa", "Afrique du Sud (l')", "ZA", "ZAF", "710"},
|
||||
{"Zimbabwe", "Zimbabwe (le)", "ZW", "ZWE", "716"},
|
||||
{"Spain", "Espagne (l')", "ES", "ESP", "724"},
|
||||
{"South Sudan", "Soudan du Sud (le)", "SS", "SSD", "728"},
|
||||
{"Sudan (the)", "Soudan (le)", "SD", "SDN", "729"},
|
||||
{"Western Sahara*", "Sahara occidental (le)*", "EH", "ESH", "732"},
|
||||
{"Suriname", "Suriname (le)", "SR", "SUR", "740"},
|
||||
{"Svalbard and Jan Mayen", "Svalbard et l'Île Jan Mayen (le)", "SJ", "SJM", "744"},
|
||||
{"Swaziland", "Swaziland (le)", "SZ", "SWZ", "748"},
|
||||
{"Sweden", "Suède (la)", "SE", "SWE", "752"},
|
||||
{"Switzerland", "Suisse (la)", "CH", "CHE", "756"},
|
||||
{"Syrian Arab Republic", "République arabe syrienne (la)", "SY", "SYR", "760"},
|
||||
{"Tajikistan", "Tadjikistan (le)", "TJ", "TJK", "762"},
|
||||
{"Thailand", "Thaïlande (la)", "TH", "THA", "764"},
|
||||
{"Togo", "Togo (le)", "TG", "TGO", "768"},
|
||||
{"Tokelau", "Tokelau (les)", "TK", "TKL", "772"},
|
||||
{"Tonga", "Tonga (les)", "TO", "TON", "776"},
|
||||
{"Trinidad and Tobago", "Trinité-et-Tobago (la)", "TT", "TTO", "780"},
|
||||
{"United Arab Emirates (the)", "Émirats arabes unis (les)", "AE", "ARE", "784"},
|
||||
{"Tunisia", "Tunisie (la)", "TN", "TUN", "788"},
|
||||
{"Turkey", "Turquie (la)", "TR", "TUR", "792"},
|
||||
{"Turkmenistan", "Turkménistan (le)", "TM", "TKM", "795"},
|
||||
{"Turks and Caicos Islands (the)", "Turks-et-Caïcos (les Îles)", "TC", "TCA", "796"},
|
||||
{"Tuvalu", "Tuvalu (les)", "TV", "TUV", "798"},
|
||||
{"Uganda", "Ouganda (l')", "UG", "UGA", "800"},
|
||||
{"Ukraine", "Ukraine (l')", "UA", "UKR", "804"},
|
||||
{"Macedonia (the former Yugoslav Republic of)", "Macédoine (l'ex‑République yougoslave de)", "MK", "MKD", "807"},
|
||||
{"Egypt", "Égypte (l')", "EG", "EGY", "818"},
|
||||
{"United Kingdom of Great Britain and Northern Ireland (the)", "Royaume-Uni de Grande-Bretagne et d'Irlande du Nord (le)", "GB", "GBR", "826"},
|
||||
{"Guernsey", "Guernesey", "GG", "GGY", "831"},
|
||||
{"Jersey", "Jersey", "JE", "JEY", "832"},
|
||||
{"Isle of Man", "Île de Man", "IM", "IMN", "833"},
|
||||
{"Tanzania, United Republic of", "Tanzanie, République-Unie de", "TZ", "TZA", "834"},
|
||||
{"United States of America (the)", "États-Unis d'Amérique (les)", "US", "USA", "840"},
|
||||
{"Virgin Islands (U.S.)", "Vierges des États-Unis (les Îles)", "VI", "VIR", "850"},
|
||||
{"Burkina Faso", "Burkina Faso (le)", "BF", "BFA", "854"},
|
||||
{"Uruguay", "Uruguay (l')", "UY", "URY", "858"},
|
||||
{"Uzbekistan", "Ouzbékistan (l')", "UZ", "UZB", "860"},
|
||||
{"Venezuela (Bolivarian Republic of)", "Venezuela (République bolivarienne du)", "VE", "VEN", "862"},
|
||||
{"Wallis and Futuna", "Wallis-et-Futuna", "WF", "WLF", "876"},
|
||||
{"Samoa", "Samoa (le)", "WS", "WSM", "882"},
|
||||
{"Yemen", "Yémen (le)", "YE", "YEM", "887"},
|
||||
{"Zambia", "Zambie (la)", "ZM", "ZMB", "894"},
|
||||
}
|
||||
268
vendor/github.com/asaskevich/govalidator/utils.go
generated
vendored
@@ -1,268 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"math"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Contains check if the string contains the substring.
|
||||
func Contains(str, substring string) bool {
|
||||
return strings.Contains(str, substring)
|
||||
}
|
||||
|
||||
// Matches check if string matches the pattern (pattern is regular expression)
|
||||
// In case of error return false
|
||||
func Matches(str, pattern string) bool {
|
||||
match, _ := regexp.MatchString(pattern, str)
|
||||
return match
|
||||
}
|
||||
|
||||
// LeftTrim trim characters from the left-side of the input.
|
||||
// If second argument is empty, it's will be remove leading spaces.
|
||||
func LeftTrim(str, chars string) string {
|
||||
pattern := ""
|
||||
if chars == "" {
|
||||
pattern = "^\\s+"
|
||||
} else {
|
||||
pattern = "^[" + chars + "]+"
|
||||
}
|
||||
r, _ := regexp.Compile(pattern)
|
||||
return string(r.ReplaceAll([]byte(str), []byte("")))
|
||||
}
|
||||
|
||||
// RightTrim trim characters from the right-side of the input.
|
||||
// If second argument is empty, it's will be remove spaces.
|
||||
func RightTrim(str, chars string) string {
|
||||
pattern := ""
|
||||
if chars == "" {
|
||||
pattern = "\\s+$"
|
||||
} else {
|
||||
pattern = "[" + chars + "]+$"
|
||||
}
|
||||
r, _ := regexp.Compile(pattern)
|
||||
return string(r.ReplaceAll([]byte(str), []byte("")))
|
||||
}
|
||||
|
||||
// Trim trim characters from both sides of the input.
|
||||
// If second argument is empty, it's will be remove spaces.
|
||||
func Trim(str, chars string) string {
|
||||
return LeftTrim(RightTrim(str, chars), chars)
|
||||
}
|
||||
|
||||
// WhiteList remove characters that do not appear in the whitelist.
|
||||
func WhiteList(str, chars string) string {
|
||||
pattern := "[^" + chars + "]+"
|
||||
r, _ := regexp.Compile(pattern)
|
||||
return string(r.ReplaceAll([]byte(str), []byte("")))
|
||||
}
|
||||
|
||||
// BlackList remove characters that appear in the blacklist.
|
||||
func BlackList(str, chars string) string {
|
||||
pattern := "[" + chars + "]+"
|
||||
r, _ := regexp.Compile(pattern)
|
||||
return string(r.ReplaceAll([]byte(str), []byte("")))
|
||||
}
|
||||
|
||||
// StripLow remove characters with a numerical value < 32 and 127, mostly control characters.
|
||||
// If keep_new_lines is true, newline characters are preserved (\n and \r, hex 0xA and 0xD).
|
||||
func StripLow(str string, keepNewLines bool) string {
|
||||
chars := ""
|
||||
if keepNewLines {
|
||||
chars = "\x00-\x09\x0B\x0C\x0E-\x1F\x7F"
|
||||
} else {
|
||||
chars = "\x00-\x1F\x7F"
|
||||
}
|
||||
return BlackList(str, chars)
|
||||
}
|
||||
|
||||
// ReplacePattern replace regular expression pattern in string
|
||||
func ReplacePattern(str, pattern, replace string) string {
|
||||
r, _ := regexp.Compile(pattern)
|
||||
return string(r.ReplaceAll([]byte(str), []byte(replace)))
|
||||
}
|
||||
|
||||
// Escape replace <, >, & and " with HTML entities.
|
||||
var Escape = html.EscapeString
|
||||
|
||||
func addSegment(inrune, segment []rune) []rune {
|
||||
if len(segment) == 0 {
|
||||
return inrune
|
||||
}
|
||||
if len(inrune) != 0 {
|
||||
inrune = append(inrune, '_')
|
||||
}
|
||||
inrune = append(inrune, segment...)
|
||||
return inrune
|
||||
}
|
||||
|
||||
// UnderscoreToCamelCase converts from underscore separated form to camel case form.
|
||||
// Ex.: my_func => MyFunc
|
||||
func UnderscoreToCamelCase(s string) string {
|
||||
return strings.Replace(strings.Title(strings.Replace(strings.ToLower(s), "_", " ", -1)), " ", "", -1)
|
||||
}
|
||||
|
||||
// CamelCaseToUnderscore converts from camel case form to underscore separated form.
|
||||
// Ex.: MyFunc => my_func
|
||||
func CamelCaseToUnderscore(str string) string {
|
||||
var output []rune
|
||||
var segment []rune
|
||||
for _, r := range str {
|
||||
if !unicode.IsLower(r) {
|
||||
output = addSegment(output, segment)
|
||||
segment = nil
|
||||
}
|
||||
segment = append(segment, unicode.ToLower(r))
|
||||
}
|
||||
output = addSegment(output, segment)
|
||||
return string(output)
|
||||
}
|
||||
|
||||
// Reverse return reversed string
|
||||
func Reverse(s string) string {
|
||||
r := []rune(s)
|
||||
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
|
||||
r[i], r[j] = r[j], r[i]
|
||||
}
|
||||
return string(r)
|
||||
}
|
||||
|
||||
// GetLines split string by "\n" and return array of lines
|
||||
func GetLines(s string) []string {
|
||||
return strings.Split(s, "\n")
|
||||
}
|
||||
|
||||
// GetLine return specified line of multiline string
|
||||
func GetLine(s string, index int) (string, error) {
|
||||
lines := GetLines(s)
|
||||
if index < 0 || index >= len(lines) {
|
||||
return "", errors.New("line index out of bounds")
|
||||
}
|
||||
return lines[index], nil
|
||||
}
|
||||
|
||||
// RemoveTags remove all tags from HTML string
|
||||
func RemoveTags(s string) string {
|
||||
return ReplacePattern(s, "<[^>]*>", "")
|
||||
}
|
||||
|
||||
// SafeFileName return safe string that can be used in file names
|
||||
func SafeFileName(str string) string {
|
||||
name := strings.ToLower(str)
|
||||
name = path.Clean(path.Base(name))
|
||||
name = strings.Trim(name, " ")
|
||||
separators, err := regexp.Compile(`[ &_=+:]`)
|
||||
if err == nil {
|
||||
name = separators.ReplaceAllString(name, "-")
|
||||
}
|
||||
legal, err := regexp.Compile(`[^[:alnum:]-.]`)
|
||||
if err == nil {
|
||||
name = legal.ReplaceAllString(name, "")
|
||||
}
|
||||
for strings.Contains(name, "--") {
|
||||
name = strings.Replace(name, "--", "-", -1)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// NormalizeEmail canonicalize an email address.
|
||||
// The local part of the email address is lowercased for all domains; the hostname is always lowercased and
|
||||
// the local part of the email address is always lowercased for hosts that are known to be case-insensitive (currently only GMail).
|
||||
// Normalization follows special rules for known providers: currently, GMail addresses have dots removed in the local part and
|
||||
// are stripped of tags (e.g. some.one+tag@gmail.com becomes someone@gmail.com) and all @googlemail.com addresses are
|
||||
// normalized to @gmail.com.
|
||||
func NormalizeEmail(str string) (string, error) {
|
||||
if !IsEmail(str) {
|
||||
return "", fmt.Errorf("%s is not an email", str)
|
||||
}
|
||||
parts := strings.Split(str, "@")
|
||||
parts[0] = strings.ToLower(parts[0])
|
||||
parts[1] = strings.ToLower(parts[1])
|
||||
if parts[1] == "gmail.com" || parts[1] == "googlemail.com" {
|
||||
parts[1] = "gmail.com"
|
||||
parts[0] = strings.Split(ReplacePattern(parts[0], `\.`, ""), "+")[0]
|
||||
}
|
||||
return strings.Join(parts, "@"), nil
|
||||
}
|
||||
|
||||
// Truncate a string to the closest length without breaking words.
|
||||
func Truncate(str string, length int, ending string) string {
|
||||
var aftstr, befstr string
|
||||
if len(str) > length {
|
||||
words := strings.Fields(str)
|
||||
before, present := 0, 0
|
||||
for i := range words {
|
||||
befstr = aftstr
|
||||
before = present
|
||||
aftstr = aftstr + words[i] + " "
|
||||
present = len(aftstr)
|
||||
if present > length && i != 0 {
|
||||
if (length - before) < (present - length) {
|
||||
return Trim(befstr, " /\\.,\"'#!?&@+-") + ending
|
||||
}
|
||||
return Trim(aftstr, " /\\.,\"'#!?&@+-") + ending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// Pad left side of string if size of string is less then indicated pad length
|
||||
func PadLeft(str string, padStr string, padLen int) string {
|
||||
return buildPadStr(str, padStr, padLen, true, false)
|
||||
}
|
||||
|
||||
// Pad right side of string if size of string is less then indicated pad length
|
||||
func PadRight(str string, padStr string, padLen int) string {
|
||||
return buildPadStr(str, padStr, padLen, false, true)
|
||||
}
|
||||
|
||||
// Pad both sides of string if size of string is less then indicated pad length
|
||||
func PadBoth(str string, padStr string, padLen int) string {
|
||||
return buildPadStr(str, padStr, padLen, true, true)
|
||||
}
|
||||
|
||||
// Pad string either left, right or both sides, not the padding string can be unicode and more then one
|
||||
// character
|
||||
func buildPadStr(str string, padStr string, padLen int, padLeft bool, padRight bool) string {
|
||||
|
||||
// When padded length is less then the current string size
|
||||
if padLen < utf8.RuneCountInString(str) {
|
||||
return str
|
||||
}
|
||||
|
||||
padLen -= utf8.RuneCountInString(str)
|
||||
|
||||
targetLen := padLen
|
||||
|
||||
targetLenLeft := targetLen
|
||||
targetLenRight := targetLen
|
||||
if padLeft && padRight {
|
||||
targetLenLeft = padLen / 2
|
||||
targetLenRight = padLen - targetLenLeft
|
||||
}
|
||||
|
||||
strToRepeatLen := utf8.RuneCountInString(padStr)
|
||||
|
||||
repeatTimes := int(math.Ceil(float64(targetLen) / float64(strToRepeatLen)))
|
||||
repeatedString := strings.Repeat(padStr, repeatTimes)
|
||||
|
||||
leftSide := ""
|
||||
if padLeft {
|
||||
leftSide = repeatedString[0:targetLenLeft]
|
||||
}
|
||||
|
||||
rightSide := ""
|
||||
if padRight {
|
||||
rightSide = repeatedString[0:targetLenRight]
|
||||
}
|
||||
|
||||
return leftSide + str + rightSide
|
||||
}
|
||||
1022
vendor/github.com/asaskevich/govalidator/validator.go
generated
vendored
15
vendor/github.com/asaskevich/govalidator/wercker.yml
generated
vendored
@@ -1,15 +0,0 @@
|
||||
box: golang
|
||||
build:
|
||||
steps:
|
||||
- setup-go-workspace
|
||||
|
||||
- script:
|
||||
name: go get
|
||||
code: |
|
||||
go version
|
||||
go get -t ./...
|
||||
|
||||
- script:
|
||||
name: go test
|
||||
code: |
|
||||
go test -race ./...
|
||||
27
vendor/github.com/gliderlabs/ssh/LICENSE
generated
vendored
@@ -1,27 +0,0 @@
|
||||
Copyright (c) 2016 Glider Labs. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Glider Labs nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
96
vendor/github.com/gliderlabs/ssh/README.md
generated
vendored
@@ -1,96 +0,0 @@
|
||||
# gliderlabs/ssh
|
||||
|
||||
[](https://godoc.org/github.com/gliderlabs/ssh)
|
||||
[](https://circleci.com/gh/gliderlabs/ssh)
|
||||
[](https://goreportcard.com/report/github.com/gliderlabs/ssh)
|
||||
[](#sponsors)
|
||||
[](http://slack.gliderlabs.com)
|
||||
[](https://app.convertkit.com/landing_pages/243312)
|
||||
|
||||
> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member
|
||||
|
||||
This Go package wraps the [crypto/ssh
|
||||
package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for
|
||||
building SSH servers. The goal of the API was to make it as simple as using
|
||||
[net/http](https://golang.org/pkg/net/http/), so the API is very similar:
|
||||
|
||||
```
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gliderlabs/ssh"
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ssh.Handle(func(s ssh.Session) {
|
||||
io.WriteString(s, "Hello world\n")
|
||||
})
|
||||
|
||||
log.Fatal(ssh.ListenAndServe(":2222", nil))
|
||||
}
|
||||
|
||||
```
|
||||
This package was built by [@progrium](https://twitter.com/progrium) after working on nearly a dozen projects at Glider Labs using SSH and collaborating with [@shazow](https://twitter.com/shazow) (known for [ssh-chat](https://github.com/shazow/ssh-chat)).
|
||||
|
||||
## Examples
|
||||
|
||||
A bunch of great examples are in the `_examples` directory.
|
||||
|
||||
## Usage
|
||||
|
||||
[See GoDoc reference.](https://godoc.org/github.com/gliderlabs/ssh)
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are welcome! However, since this project is very much about API
|
||||
design, please submit API changes as issues to discuss before submitting PRs.
|
||||
|
||||
Also, you can [join our Slack](http://slack.gliderlabs.com) to discuss as well.
|
||||
|
||||
## Roadmap
|
||||
|
||||
* Non-session channel handlers
|
||||
* Cleanup callback API
|
||||
* 1.0 release
|
||||
* High-level client?
|
||||
|
||||
## Sponsors
|
||||
|
||||
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/ssh#sponsor)]
|
||||
|
||||
<a href="https://opencollective.com/ssh/sponsor/0/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/1/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/2/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/3/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/4/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/5/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/6/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/7/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/8/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/9/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/10/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/10/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/11/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/11/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/12/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/12/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/13/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/13/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/14/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/14/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/15/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/15/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/16/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/16/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/17/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/17/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/18/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/18/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/19/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/19/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/20/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/20/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/21/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/21/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/22/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/22/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/23/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/23/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/24/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/24/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/25/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/25/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/26/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/26/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/27/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/27/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/28/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/28/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/29/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/29/avatar.svg"></a>
|
||||
|
||||
## License
|
||||
|
||||
BSD
|
||||
81
vendor/github.com/gliderlabs/ssh/agent.go
generated
vendored
@@ -1,81 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
agentRequestType = "auth-agent-req@openssh.com"
|
||||
agentChannelType = "auth-agent@openssh.com"
|
||||
|
||||
agentTempDir = "auth-agent"
|
||||
agentListenFile = "listener.sock"
|
||||
)
|
||||
|
||||
// contextKeyAgentRequest is an internal context key for storing if the
|
||||
// client requested agent forwarding
|
||||
var contextKeyAgentRequest = &contextKey{"auth-agent-req"}
|
||||
|
||||
func setAgentRequested(sess *session) {
|
||||
sess.ctx.SetValue(contextKeyAgentRequest, true)
|
||||
}
|
||||
|
||||
// AgentRequested returns true if the client requested agent forwarding.
|
||||
func AgentRequested(sess Session) bool {
|
||||
return sess.Context().Value(contextKeyAgentRequest) == true
|
||||
}
|
||||
|
||||
// NewAgentListener sets up a temporary Unix socket that can be communicated
|
||||
// to the session environment and used for forwarding connections.
|
||||
func NewAgentListener() (net.Listener, error) {
|
||||
dir, err := ioutil.TempDir("", agentTempDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l, err := net.Listen("unix", path.Join(dir, agentListenFile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ForwardAgentConnections takes connections from a listener to proxy into the
|
||||
// session on the OpenSSH channel for agent connections. It blocks and services
|
||||
// connections until the listener stop accepting.
|
||||
func ForwardAgentConnections(l net.Listener, s Session) {
|
||||
sshConn := s.Context().Value(ContextKeyConn).(gossh.Conn)
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
channel, reqs, err := sshConn.OpenChannel(agentChannelType, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer channel.Close()
|
||||
go gossh.DiscardRequests(reqs)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
io.Copy(conn, channel)
|
||||
conn.(*net.UnixConn).CloseWrite()
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
io.Copy(channel, conn)
|
||||
channel.CloseWrite()
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
10
vendor/github.com/gliderlabs/ssh/circle.yml
generated
vendored
@@ -1,10 +0,0 @@
|
||||
version: 2.0
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: golang:1.8
|
||||
working_directory: /go/src/github.com/gliderlabs/ssh
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get
|
||||
- run: go test -v -race
|
||||
55
vendor/github.com/gliderlabs/ssh/conn.go
generated
vendored
@@ -1,55 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type serverConn struct {
|
||||
net.Conn
|
||||
|
||||
idleTimeout time.Duration
|
||||
maxDeadline time.Time
|
||||
closeCanceler context.CancelFunc
|
||||
}
|
||||
|
||||
func (c *serverConn) Write(p []byte) (n int, err error) {
|
||||
c.updateDeadline()
|
||||
n, err = c.Conn.Write(p)
|
||||
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
|
||||
c.closeCanceler()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *serverConn) Read(b []byte) (n int, err error) {
|
||||
c.updateDeadline()
|
||||
n, err = c.Conn.Read(b)
|
||||
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
|
||||
c.closeCanceler()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *serverConn) Close() (err error) {
|
||||
err = c.Conn.Close()
|
||||
if c.closeCanceler != nil {
|
||||
c.closeCanceler()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *serverConn) updateDeadline() {
|
||||
switch {
|
||||
case c.idleTimeout > 0:
|
||||
idleDeadline := time.Now().Add(c.idleTimeout)
|
||||
if idleDeadline.Unix() < c.maxDeadline.Unix() {
|
||||
c.Conn.SetDeadline(idleDeadline)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
c.Conn.SetDeadline(c.maxDeadline)
|
||||
}
|
||||
}
|
||||
148
vendor/github.com/gliderlabs/ssh/context.go
generated
vendored
@@ -1,148 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// contextKey is a value for use with context.WithValue. It's used as
|
||||
// a pointer so it fits in an interface{} without allocation.
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var (
|
||||
// ContextKeyUser is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type string.
|
||||
ContextKeyUser = &contextKey{"user"}
|
||||
|
||||
// ContextKeySessionID is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type string.
|
||||
ContextKeySessionID = &contextKey{"session-id"}
|
||||
|
||||
// ContextKeyPermissions is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type *Permissions.
|
||||
ContextKeyPermissions = &contextKey{"permissions"}
|
||||
|
||||
// ContextKeyClientVersion is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type string.
|
||||
ContextKeyClientVersion = &contextKey{"client-version"}
|
||||
|
||||
// ContextKeyServerVersion is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type string.
|
||||
ContextKeyServerVersion = &contextKey{"server-version"}
|
||||
|
||||
// ContextKeyLocalAddr is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type net.Addr.
|
||||
ContextKeyLocalAddr = &contextKey{"local-addr"}
|
||||
|
||||
// ContextKeyRemoteAddr is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type net.Addr.
|
||||
ContextKeyRemoteAddr = &contextKey{"remote-addr"}
|
||||
|
||||
// ContextKeyServer is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type *Server.
|
||||
ContextKeyServer = &contextKey{"ssh-server"}
|
||||
|
||||
// ContextKeyConn is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type gossh.Conn.
|
||||
ContextKeyConn = &contextKey{"ssh-conn"}
|
||||
|
||||
// ContextKeyPublicKey is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type PublicKey.
|
||||
ContextKeyPublicKey = &contextKey{"public-key"}
|
||||
)
|
||||
|
||||
// Context is a package specific context interface. It exposes connection
|
||||
// metadata and allows new values to be easily written to it. It's used in
|
||||
// authentication handlers and callbacks, and its underlying context.Context is
|
||||
// exposed on Session in the session Handler.
|
||||
type Context interface {
|
||||
context.Context
|
||||
|
||||
// User returns the username used when establishing the SSH connection.
|
||||
User() string
|
||||
|
||||
// SessionID returns the session hash.
|
||||
SessionID() string
|
||||
|
||||
// ClientVersion returns the version reported by the client.
|
||||
ClientVersion() string
|
||||
|
||||
// ServerVersion returns the version reported by the server.
|
||||
ServerVersion() string
|
||||
|
||||
// RemoteAddr returns the remote address for this connection.
|
||||
RemoteAddr() net.Addr
|
||||
|
||||
// LocalAddr returns the local address for this connection.
|
||||
LocalAddr() net.Addr
|
||||
|
||||
// Permissions returns the Permissions object used for this connection.
|
||||
Permissions() *Permissions
|
||||
|
||||
// SetValue allows you to easily write new values into the underlying context.
|
||||
SetValue(key, value interface{})
|
||||
}
|
||||
|
||||
type sshContext struct {
|
||||
context.Context
|
||||
}
|
||||
|
||||
func newContext(srv *Server) (*sshContext, context.CancelFunc) {
|
||||
innerCtx, cancel := context.WithCancel(context.Background())
|
||||
ctx := &sshContext{innerCtx}
|
||||
ctx.SetValue(ContextKeyServer, srv)
|
||||
perms := &Permissions{&gossh.Permissions{}}
|
||||
ctx.SetValue(ContextKeyPermissions, perms)
|
||||
return ctx, cancel
|
||||
}
|
||||
|
||||
// this is separate from newContext because we will get ConnMetadata
|
||||
// at different points so it needs to be applied separately
|
||||
func (ctx *sshContext) applyConnMetadata(conn gossh.ConnMetadata) {
|
||||
if ctx.Value(ContextKeySessionID) != nil {
|
||||
return
|
||||
}
|
||||
ctx.SetValue(ContextKeySessionID, hex.EncodeToString(conn.SessionID()))
|
||||
ctx.SetValue(ContextKeyClientVersion, string(conn.ClientVersion()))
|
||||
ctx.SetValue(ContextKeyServerVersion, string(conn.ServerVersion()))
|
||||
ctx.SetValue(ContextKeyUser, conn.User())
|
||||
ctx.SetValue(ContextKeyLocalAddr, conn.LocalAddr())
|
||||
ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr())
|
||||
}
|
||||
|
||||
func (ctx *sshContext) SetValue(key, value interface{}) {
|
||||
ctx.Context = context.WithValue(ctx.Context, key, value)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) User() string {
|
||||
return ctx.Value(ContextKeyUser).(string)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) SessionID() string {
|
||||
return ctx.Value(ContextKeySessionID).(string)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) ClientVersion() string {
|
||||
return ctx.Value(ContextKeyClientVersion).(string)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) ServerVersion() string {
|
||||
return ctx.Value(ContextKeyServerVersion).(string)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) RemoteAddr() net.Addr {
|
||||
return ctx.Value(ContextKeyRemoteAddr).(net.Addr)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) LocalAddr() net.Addr {
|
||||
return ctx.Value(ContextKeyLocalAddr).(net.Addr)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) Permissions() *Permissions {
|
||||
return ctx.Value(ContextKeyPermissions).(*Permissions)
|
||||
}
|
||||