Compare commits

..

110 Commits

Author SHA1 Message Date
Manfred Touron
be83c7148d Merge pull request #172 from moul/dev/moul/bump-deps4 2020-05-12 03:35:22 +02:00
Manfred Touron
ce187e8675 fix: bump deps 2020-05-12 03:30:27 +02:00
Manfred Touron
f13ede4ba7 Update README.md 2020-05-12 03:27:15 +02:00
Manfred Touron
fb061ed419 Merge pull request #138 from moul/renovate/all
Update all
2020-05-10 15:45:44 +02:00
Renovate Bot
b4a377f269 Update all 2020-05-02 08:14:28 +00:00
Manfred Touron
de6f37aa64 Merge pull request #170 from moul/renovate/docker-all
Update all Docker tags to v1.14.2
2020-04-10 10:11:05 +02:00
Renovate Bot
32219577b8 Update all Docker tags to v1.14.2 2020-04-10 00:21:11 +00:00
Manfred Touron
abc7329a71 Merge pull request #167 from moul/renovate/docker-all
Update all Docker tags to v1.14.1
2020-03-22 14:58:30 +01:00
Renovate Bot
675942e967 Update all Docker tags to v1.14.1 2020-03-21 00:28:54 +00:00
Manfred Touron
5b20cd501e Update README.md
See https://github.com/moul/sshportal/issues/164
2020-03-10 11:01:26 +01:00
Manfred Touron
b6aaf4d7cf Update README.md 2020-03-05 15:33:52 +01:00
Manfred Touron
972e232559 Merge pull request #143 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.14.0
2020-03-01 17:17:37 +01:00
Manfred Touron
851a91b1a0 fix: fix deps 2020-02-28 22:12:46 +01:00
Renovate Bot
6a068dc430 chore(deps): update all docker tags to v1.14.0 2020-02-28 22:05:37 +01:00
Manfred Touron
2cdfcf60fe Merge pull request #163 from MitaliBo/master
Security fix for github.com/moby/moby
2020-02-28 22:04:24 +01:00
Manfred Touron
5d9e0c367a feat: use secure rand seed 2020-02-25 11:49:38 +01:00
MitaliBo
cbf8263033 Update go.mod 2020-02-24 14:27:29 -08:00
Manfred Touron
846c73d9bc Merge pull request #160 from moul/dev/moul/bump-deps
chore: bump-deps
2020-02-22 20:51:29 +01:00
Manfred Touron
e0b43b1976 Merge pull request #162 from moul/dev/moul/semantic-release
feat: set up semantic release
2020-02-22 20:48:51 +01:00
Manfred Touron
6a6e788968 feat: set up semantic release 2020-02-22 20:43:54 +01:00
Manfred Touron
4754cad42a chore: bump-deps 2020-02-18 00:12:56 +01:00
Manfred Touron
db58e53f3b Merge pull request #156 from jeanlouisferey/master
Little correction
2020-02-18 00:06:27 +01:00
Manfred Touron
b31acb4348 Merge pull request #148 from jle64/fix_shell_connection_log
Fix db user id type in shell connection log.
2020-02-18 00:06:20 +01:00
Manfred Touron
c794c2c076 chore: add funding + update go.sum 2020-02-18 00:05:35 +01:00
Jonathan Lestrelin
42d6cd44bb Fix db user id type in shell connection log. 2020-02-17 23:55:35 +01:00
Manfred Touron
f9057ca56a Merge pull request #157 from bozzo/master
Add Helm chart to deploy SSHPortal on Kubernetes
2020-02-17 23:54:39 +01:00
bozzo
c2f1999037 Set theme jekyll-theme-slate 2020-02-17 09:08:51 +01:00
bozzo
44b386f7a7 Add Helm chart to deploy SSHPortal on Kubernetes
Support HA with Mysql database
Support autoscaling
2020-02-08 23:06:19 +01:00
Jean-Louis Férey
89b296db4e Little correction 2020-01-29 16:35:39 +01:00
Manfred Touron
c16403fb3f Merge pull request #146 from opencollective/opencollective
Activating Open Collective
2019-09-18 09:11:08 +02:00
Jess
5e21fb72e6 Added financial contributors to the README 2019-09-17 18:02:58 -07:00
Manfred Touron
c5681bf880 chore: post-release version bump 2019-06-24 11:43:32 +02:00
Manfred Touron
db85d6545d v1.10.0 2019-06-24 11:31:32 +02:00
Manfred Touron
9912c3deba chore: setup goreleaser 2019-06-24 10:57:47 +02:00
Manfred Touron
fc5c342e40 chore: bump github.com/gliderlabs/ssh to v0.2.2 (#141)
chore: bump github.com/gliderlabs/ssh to v0.2.2
2019-06-24 10:28:51 +02:00
Manfred Touron
60707b3faa chore: update CHANGELOG 2019-06-24 10:25:04 +02:00
Manfred Touron
f36845ac6b chore: bump github.com/gliderlabs/ssh to v0.2.2 2019-06-24 10:25:04 +02:00
Manfred Touron
9f76bd6cad Merge pull request #131 from moul/renovate/all
Update all
2019-06-14 11:26:07 +02:00
Manfred Touron
c53d5d9964 Merge pull request #137 from moul/renovate/docker-all
Update all Docker tags to v1.12.6
2019-06-14 11:25:21 +02:00
Renovate Bot
171d461ea5 Update all 2019-06-14 08:29:13 +00:00
Renovate Bot
b2b04a1155 Update all Docker tags to v1.12.6 2019-06-13 00:27:25 +00:00
Manfred Touron
671ba03b78 Update README.md 2019-06-10 09:57:20 +02:00
Manfred Touron
9095725778 Update README.md 2019-06-10 09:57:05 +02:00
Manfred Touron
8b2e5daba3 Set log files mode to 440 instead of 640. (#134)
Set log files mode to 440 instead of 640.
2019-06-10 09:54:53 +02:00
Jonathan Lestrelin
75b7a5f571 Set log files mode to 440 instead of 640. 2019-06-10 09:40:25 +02:00
Manfred Touron
4b9e881ad0 Merge pull request #135 from jle64/accept_ip_as_hostname
Allow to create a host using an ip as name
2019-06-09 15:39:41 +02:00
Manfred Touron
59f8f52cca Merge pull request #133 from jle64/better_logfile_name
Add username and session ID to session log filename.
2019-06-09 15:38:08 +02:00
Jonathan Lestrelin
4adaf83fd3 Accept to create a host using an ip as name by relaxing unix_user constraint on name and not splitting on . if an ip is detected. 2019-06-07 18:15:25 +02:00
Jonathan Lestrelin
84464a4ea6 Add username and session ID to session log filename. 2019-06-07 16:58:41 +02:00
Manfred Touron
cafac0b8b5 chore: fix CI (#132)
chore: fix CI
2019-06-06 16:35:45 +02:00
Manfred Touron
5346300a64 chore: fix CI 2019-06-06 16:32:49 +02:00
Manfred Touron
1d4554eabc chore: fixup 2019-06-06 16:32:22 +02:00
Manfred Touron
50bdba8b70 Update all (#110)
Update all
2019-05-30 20:40:22 +02:00
Manfred Touron
8c785f6dea Update all Docker tags to v1.12.5 (#130)
Update all Docker tags to v1.12.5
2019-05-30 20:40:01 +02:00
Renovate Bot
93e6abc9ba Update all 2019-05-28 19:27:01 +00:00
Renovate Bot
60d7c85c11 Update all Docker tags to v1.12.5 2019-05-21 20:28:40 +00:00
Manfred Touron
883bad2ee5 Merge pull request #124 from welderpb/master
[fix] unable to use encrypted ssh private keys
2019-03-29 07:11:08 +01:00
Manfred Touron
7d68e144f8 Merge pull request #116 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.12.1
2019-03-29 07:08:36 +01:00
Manfred Touron
7f32e38cf8 Merge pull request #123 from vdaviot/master
[FIX] Format of id in new session | Closing channel if host unreachable
2019-03-29 07:07:36 +01:00
Renovate Bot
43a96d1636 chore(deps): update all docker tags to v1.12.1 2019-03-16 00:20:45 +00:00
welderpb
00e7d2e45d fix for encrypted ssh private keys 2019-03-05 17:04:05 +01:00
Valentin Daviot
2e711c3591 fixed format of id for new bastion session | applied fix from issue #56 to permit the closing of channel in case of unreachable host
Signed-off-by: Valentin Daviot <valentin.daviot@alterway.fr>
2019-03-01 16:04:27 +01:00
Manfred Touron
5d147fc03b Merge pull request #118 from nellyasher/master
Readme updates
2019-02-01 13:38:07 +01:00
Manfred Touron
3dccefbbcb chore: use png + add toc 2019-02-01 13:36:36 +01:00
Nelly Asher
7c4995fa4a Update README.md 2019-02-01 13:34:18 +01:00
Nelly Asher
2b8f051414 TOC added, chapters rearranged, "Use cases" added. 2019-02-01 13:34:18 +01:00
Manfred Touron
4e17c81d63 chore: switch to png for images in the readme 2019-02-01 12:57:18 +01:00
Manfred Touron
8b4b677d6a chore: update flow diagram + add png outputs 2019-02-01 12:56:07 +01:00
Manfred Touron
47229bf473 chore: update flow diagram style 2019-02-01 11:23:02 +01:00
Manfred Touron
ec5b567da9 Update README.md 2019-01-04 01:11:10 +01:00
Manfred Touron
03b59fae1c Merge pull request #113 from ahhx/master
refactor: split package main
2019-01-04 00:58:24 +01:00
Manfred Touron
ede8b3ecf2 chore: fix lint issues 2019-01-04 00:56:06 +01:00
Manfred Touron
7ae90b9199 build: fix deps checksums 2019-01-04 00:51:54 +01:00
ahh
a651da451e refactor: split package main
sshportal refactor. Focused on splitting up package main into packages
main, dbmodels, crypto, and bastion.
2019-01-03 21:11:43 +01:00
Manfred Touron
f220af5c54 Merge pull request #111 from shawn111/missing_authorized_key
Fix userkey create.
2019-01-03 19:57:19 +01:00
Shawn Wang
eebf987900 Fix UserKey.Key for govalidator.ValidateStruct.
UserKey.Key is a []byte field.
The valid: "required" would cause wrong Validation.
2018-12-13 10:46:05 +00:00
Shawn Wang
3d5101011f Fix userkey create, missing AuthorizedKey 2018-12-12 23:39:25 +08:00
Manfred Touron
2cdc19dfdd Merge pull request #109 from moul/moul-patch-1
Update renovate.json
2018-12-07 14:53:50 +01:00
Manfred Touron
d8a7b1e16c Update renovate.json 2018-12-07 14:49:50 +01:00
Manfred Touron
d7490d089c Merge pull request #102 from moul/dev/mou/bump-deps2
chore: bump deps
2018-11-25 09:46:57 +01:00
Manfred Touron
774c6c0f64 chore: bump deps 2018-11-25 09:43:28 +01:00
Manfred Touron
92d11c53de Merge pull request #98 from Raerten/env-variables
use Environment Variables for settings
2018-11-25 09:41:09 +01:00
Manfred Touron
c509f65a27 Merge pull request #101 from moul/dev/moul/windows-support
fix a build constraint
2018-11-25 09:40:00 +01:00
Дмитрий Шульгачик
20b9e839d3 fix a build constraint 2018-11-25 09:34:41 +01:00
Дмитрий Шульгачик
6f4fb24cd0 update mysql example using ENV 2018-11-19 14:56:24 +03:00
Дмитрий Шульгачик
23a89fe1de Add ability to set DB driver and DB credentials from environment variables 2018-11-19 14:49:31 +03:00
Manfred Touron
559df1f523 Merge pull request #96 from moul/dev/moul/bump-deps
chore: bump deps
2018-11-18 15:58:07 +01:00
Manfred Touron
ad2b8ebc38 chore: bump deps 2018-11-18 15:54:11 +01:00
Manfred Touron
3824629d4d Post-release version bump 2018-11-18 15:48:42 +01:00
Manfred Touron
38224714e1 v1.9.0 2018-11-18 15:46:55 +01:00
Manfred Touron
a9f4227bba Merge pull request #90 from alenn-m/master
fix #69
2018-11-18 15:32:37 +01:00
Alen Masic
06bde77f51 fixed the issue when Author didn't update 2018-11-17 10:39:34 +01:00
Alen Masic
2a2554e7a3 fix #69 2018-11-16 17:31:59 +01:00
Manfred Touron
5d835011e6 Merge pull request #87 from moul/dev/moul/golangci-lint
build: switch to golangci-lint
2018-11-16 17:26:26 +01:00
Manfred Touron
0f294cd62d build: switch to golangci-lint 2018-11-16 17:20:39 +01:00
Manfred Touron
8e62d21c25 Merge pull request #86 from moul/dev/moul/canonical
Switching to moul.io/sshportal (canonical url)
2018-11-16 15:42:38 +01:00
Manfred Touron
2a5dd63e87 chore: use moul.io/sshportal canonical url 2018-11-16 15:36:14 +01:00
Manfred Touron
06cb424b8f chore: bump deps 2018-11-16 15:29:51 +01:00
Manfred Touron
7c5864a9c3 Merge pull request #83 from moul/dev/moul/gomod
build: switch to go modules
2018-11-16 15:17:22 +01:00
Manfred Touron
668e34ccab build: switch to go modules 2018-11-16 15:02:47 +01:00
Manfred Touron
95477715fc Merge pull request #75 from vdaviot/closed_session
[FIX] session status and duration displaying properly
2018-11-16 10:19:38 +01:00
Manfred Touron
ecc004a485 Merge pull request #74 from vdaviot/unset_hop
[FIX] unsetting hop on host
2018-11-16 10:18:10 +01:00
valentin.daviot
9f0657374b unsetting work properly 2018-11-16 10:12:49 +01:00
Manfred Touron
61b7f72e94 Merge pull request #76 from vdaviot/active_session_list
[FIX] Session ls on big databases
2018-11-16 10:10:30 +01:00
Manfred Touron
db000baaa5 Merge pull request #67 from moul/renovate/configure
Configure Renovate
2018-11-16 10:05:42 +01:00
valentin.daviot
a1a3a29d00 session status and duration displaying properly 2018-11-16 10:03:37 +01:00
valentin.daviot
2ea73a941f listing active session 2018-11-16 10:01:23 +01:00
valentin.daviot
e860b60d20 session status and duration displaying properly 2018-11-16 10:01:23 +01:00
Manfred Touron
d6be01b9b7 build: temporarily disable gometalinter before changing to another solution 2018-11-16 09:59:18 +01:00
Renovate Bot
b9d1cf69c7 Add renovate.json 2018-08-19 01:32:26 +00:00
585 changed files with 1859 additions and 413567 deletions

BIN
.assets/cluster-mysql.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,12 +1,12 @@
digraph {
rankdir=LR;
layout=dot;
node[shape=record];
start[label="ssh sshportal";color=blue;fontcolor=blue;fontsize=20];
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=20;style=dashed;color=purple;fontcolor=purple];
graph[fontsize=18;color=gray;fontcolor=black];
label="sshportal";
{
node[color=darkorange;fontcolor=darkorange];
@@ -17,25 +17,25 @@ digraph {
}
{
node[color=darkgreen;fontcolor=darkgreen];
builtin_shell[label="built-in shell"];
ssh_proxy[label="SSH proxy"];
learn_key[label="learn key"];
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="error and exit";color=red;fontcolor=red];
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=20;style=dashed;color=purple;fontcolor=purple];
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;
@@ -53,13 +53,13 @@ digraph {
{
edge[color=darkorange;fontcolor=darkorange];
known_user_key -> acl_manager[label="user matches an existing host"];
unknown_user_key -> invite_manager[headlabel="user=invite:<token>"];
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"];
invite_manager -> err_and_exit[label="invalid token"];
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -4,185 +4,175 @@
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: %3 Pages: 1 -->
<svg width="1026pt" height="312pt"
viewBox="0.00 0.00 1026.42 312.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 308)">
<svg width="1150pt" height="310pt"
viewBox="0.00 0.00 1149.83 310.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 306)">
<title>%3</title>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-308 1022.4219,-308 1022.4219,4 -4,4"/>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-306 1145.8281,-306 1145.8281,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_sshportal</title>
<polygon fill="none" stroke="#a020f0" stroke-dasharray="5,2" points="147.7832,-8 147.7832,-296 858.9775,-296 858.9775,-8 147.7832,-8"/>
<text text-anchor="middle" x="503.3804" y="-276" font-family="Times,serif" font-size="20.00" fill="#a020f0">sshportal</text>
<polygon fill="none" stroke="#c0c0c0" points="187.5586,-8 187.5586,-294 964.46,-294 964.46,-8 187.5586,-8"/>
<text text-anchor="middle" x="576.0093" y="-275.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="18.00" fill="#000000">sshportal</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_hosts</title>
<polygon fill="none" stroke="#a020f0" stroke-dasharray="5,2" points="879.9775,-104 879.9775,-296 1010.4219,-296 1010.4219,-104 879.9775,-104"/>
<text text-anchor="middle" x="945.1997" y="-276" font-family="Times,serif" font-size="20.00" fill="#a020f0">your hosts</text>
<polygon fill="none" stroke="#c0c0c0" points="985.46,-104 985.46,-294 1133.8281,-294 1133.8281,-104 985.46,-104"/>
<text text-anchor="middle" x="1059.644" y="-275.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="18.00" fill="#000000">your hosts</text>
</g>
<!-- start -->
<g id="node1" class="node">
<title>start</title>
<polygon fill="none" stroke="#0000ff" points="0,-118 0,-154 118.7832,-154 118.7832,-118 0,-118"/>
<text text-anchor="middle" x="59.3916" y="-130" font-family="Times,serif" font-size="20.00" fill="#0000ff">ssh sshportal</text>
<path fill="none" stroke="#0000ff" d="M12,-122C12,-122 146.5586,-122 146.5586,-122 152.5586,-122 158.5586,-128 158.5586,-134 158.5586,-134 158.5586,-146 158.5586,-146 158.5586,-152 152.5586,-158 146.5586,-158 146.5586,-158 12,-158 12,-158 6,-158 0,-152 0,-146 0,-146 0,-134 0,-134 0,-128 6,-122 12,-122"/>
<text text-anchor="middle" x="79.2793" y="-134.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="18.00" fill="#0000ff">$&gt; ssh sshportal</text>
</g>
<!-- known_user_key -->
<g id="node2" class="node">
<title>known_user_key</title>
<polygon fill="none" stroke="#ff8c00" points="162.7832,-157 162.7832,-193 267.4316,-193 267.4316,-157 162.7832,-157"/>
<text text-anchor="middle" x="215.1074" y="-170.8" font-family="Times,serif" font-size="14.00" fill="#ff8c00">known user key</text>
<path fill="none" stroke="#ff8c00" d="M216.1104,-161C216.1104,-161 313.1514,-161 313.1514,-161 319.1514,-161 325.1514,-167 325.1514,-173 325.1514,-173 325.1514,-185 325.1514,-185 325.1514,-191 319.1514,-197 313.1514,-197 313.1514,-197 216.1104,-197 216.1104,-197 210.1104,-197 204.1104,-191 204.1104,-185 204.1104,-185 204.1104,-173 204.1104,-173 204.1104,-167 210.1104,-161 216.1104,-161"/>
<text text-anchor="middle" x="264.6309" y="-174.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff8c00">known user key</text>
</g>
<!-- start&#45;&gt;known_user_key -->
<g id="edge1" class="edge">
<title>start&#45;&gt;known_user_key</title>
<path fill="none" stroke="#0000ff" d="M119.1501,-150.9669C130.1162,-153.7134 141.5894,-156.587 152.6326,-159.3528"/>
<polygon fill="#0000ff" stroke="#0000ff" points="152.0758,-162.8214 162.6266,-161.8558 153.7765,-156.0311 152.0758,-162.8214"/>
<path fill="none" stroke="#0000ff" d="M158.6917,-156.7092C173.8232,-159.8931 189.4365,-163.1783 203.8727,-166.2158"/>
</g>
<!-- unknown_user_key -->
<g id="node3" class="node">
<title>unknown_user_key</title>
<polygon fill="none" stroke="#ff8c00" points="155.7832,-72 155.7832,-108 274.4316,-108 274.4316,-72 155.7832,-72"/>
<text text-anchor="middle" x="215.1074" y="-85.8" font-family="Times,serif" font-size="14.00" fill="#ff8c00">unknown user key</text>
<path fill="none" stroke="#ff8c00" d="M207.5586,-69C207.5586,-69 321.7031,-69 321.7031,-69 327.7031,-69 333.7031,-75 333.7031,-81 333.7031,-81 333.7031,-93 333.7031,-93 333.7031,-99 327.7031,-105 321.7031,-105 321.7031,-105 207.5586,-105 207.5586,-105 201.5586,-105 195.5586,-99 195.5586,-93 195.5586,-93 195.5586,-81 195.5586,-81 195.5586,-75 201.5586,-69 207.5586,-69"/>
<text text-anchor="middle" x="264.6309" y="-82.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff8c00">unknown user key</text>
</g>
<!-- start&#45;&gt;unknown_user_key -->
<g id="edge2" class="edge">
<title>start&#45;&gt;unknown_user_key</title>
<path fill="none" stroke="#0000ff" d="M119.1501,-118.3468C127.968,-115.7419 137.1138,-113.0401 146.1003,-110.3854"/>
<polygon fill="#0000ff" stroke="#0000ff" points="147.1673,-113.7198 155.766,-107.5301 145.1841,-107.0066 147.1673,-113.7198"/>
<path fill="none" stroke="#0000ff" d="M142.2895,-121.9827C161.3902,-116.521 182.3703,-110.5218 201.4801,-105.0575"/>
</g>
<!-- acl_manager -->
<g id="node5" class="node">
<title>acl_manager</title>
<polygon fill="none" stroke="#ff8c00" points="514.7056,-173 514.7056,-209 609.8862,-209 609.8862,-173 514.7056,-173"/>
<text text-anchor="middle" x="562.2959" y="-186.8" font-family="Times,serif" font-size="14.00" fill="#ff8c00">ACL manager</text>
<path fill="none" stroke="#ff8c00" d="M608.9287,-173C608.9287,-173 691.7031,-173 691.7031,-173 697.7031,-173 703.7031,-179 703.7031,-185 703.7031,-185 703.7031,-197 703.7031,-197 703.7031,-203 697.7031,-209 691.7031,-209 691.7031,-209 608.9287,-209 608.9287,-209 602.9287,-209 596.9287,-203 596.9287,-197 596.9287,-197 596.9287,-185 596.9287,-185 596.9287,-179 602.9287,-173 608.9287,-173"/>
<text text-anchor="middle" x="650.3159" y="-186.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff8c00">ACL manager</text>
</g>
<!-- known_user_key&#45;&gt;acl_manager -->
<g id="edge9" class="edge">
<title>known_user_key&#45;&gt;acl_manager</title>
<path fill="none" stroke="#ff8c00" d="M267.461,-177.4127C331.1153,-180.3462 438.21,-185.2816 504.3082,-188.3277"/>
<polygon fill="#ff8c00" stroke="#ff8c00" points="504.401,-191.8356 514.5516,-188.7997 504.7233,-184.843 504.401,-191.8356"/>
<text text-anchor="middle" x="393.4697" y="-188.8" font-family="Times,serif" font-size="14.00" fill="#ff8c00">user matches an existing host</text>
<path fill="none" stroke="#ff8c00" d="M325.3184,-180.8882C399.9907,-183.2115 525.6007,-187.1197 596.8147,-189.3354"/>
<text text-anchor="middle" x="463.3062" y="-190.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff8c00">user matches an existing host</text>
</g>
<!-- builtin_shell -->
<g id="node6" class="node">
<title>builtin_shell</title>
<polygon fill="none" stroke="#006400" points="761.6929,-223 761.6929,-259 848.855,-259 848.855,-223 761.6929,-223"/>
<text text-anchor="middle" x="805.2739" y="-236.8" font-family="Times,serif" font-size="14.00" fill="#006400">built&#45;in shell</text>
<path fill="none" stroke="#006400" d="M874.6738,-223C874.6738,-223 944.46,-223 944.46,-223 950.46,-223 956.46,-229 956.46,-235 956.46,-235 956.46,-247 956.46,-247 956.46,-253 950.46,-259 944.46,-259 944.46,-259 874.6738,-259 874.6738,-259 868.6738,-259 862.6738,-253 862.6738,-247 862.6738,-247 862.6738,-235 862.6738,-235 862.6738,-229 868.6738,-223 874.6738,-223"/>
<text text-anchor="middle" x="909.5669" y="-243.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">built&#45;in</text>
<text text-anchor="middle" x="909.5669" y="-229.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">config shell</text>
</g>
<!-- known_user_key&#45;&gt;builtin_shell -->
<g id="edge6" class="edge">
<title>known_user_key&#45;&gt;builtin_shell</title>
<path fill="none" stroke="#006400" d="M267.592,-193.0548C281.6792,-197.2785 297.0081,-201.3215 311.4316,-204 469.5409,-233.361 660.2348,-239.5693 751.4965,-240.7835"/>
<polygon fill="#006400" stroke="#006400" points="751.5568,-244.2844 761.5974,-240.9027 751.6394,-237.2848 751.5568,-244.2844"/>
<text text-anchor="middle" x="562.2959" y="-238.8" font-family="Times,serif" font-size="14.00" fill="#006400">user=admin</text>
<path fill="none" stroke="#006400" d="M325.3695,-196.5059C340.0986,-200.1819 355.8759,-203.652 370.7031,-206 550.8024,-234.5204 768.2909,-239.9067 862.3934,-240.8487"/>
<text text-anchor="middle" x="650.3159" y="-238.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#006400">user=admin</text>
</g>
<!-- err_and_exit -->
<g id="node9" class="node">
<title>err_and_exit</title>
<polygon fill="none" stroke="#ff0000" points="759.5703,-106 759.5703,-142 850.9775,-142 850.9775,-106 759.5703,-106"/>
<text text-anchor="middle" x="805.2739" y="-119.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">error and exit</text>
<path fill="none" stroke="#ff0000" d="M887.1152,-81C887.1152,-81 932.0186,-81 932.0186,-81 938.0186,-81 944.0186,-87 944.0186,-93 944.0186,-93 944.0186,-137 944.0186,-137 944.0186,-143 938.0186,-149 932.0186,-149 932.0186,-149 887.1152,-149 887.1152,-149 881.1152,-149 875.1152,-143 875.1152,-137 875.1152,-137 875.1152,-93 875.1152,-93 875.1152,-87 881.1152,-81 887.1152,-81"/>
<text text-anchor="middle" x="909.5669" y="-117.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff0000">error</text>
<text text-anchor="middle" x="909.5669" y="-103.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff0000">and exit</text>
</g>
<!-- known_user_key&#45;&gt;err_and_exit -->
<g id="edge11" class="edge">
<title>known_user_key&#45;&gt;err_and_exit</title>
<path fill="none" stroke="#ff0000" d="M267.4808,-170.4741C378.1362,-160.9117 634.8943,-138.7236 748.9418,-128.868"/>
<polygon fill="#ff0000" stroke="#ff0000" points="749.5354,-132.3298 759.1969,-127.9818 748.9327,-125.3558 749.5354,-132.3298"/>
<text text-anchor="middle" x="562.2959" y="-151.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">invalid user</text>
<path fill="none" stroke="#ff0000" d="M325.3049,-172.979C457.9451,-159.8165 770.2119,-128.8288 874.7656,-118.4535"/>
<text text-anchor="middle" x="650.3159" y="-148.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff0000">invalid user</text>
</g>
<!-- invite_manager -->
<g id="node4" class="node">
<title>invite_manager</title>
<polygon fill="none" stroke="#ff8c00" points="512.5078,-17 512.5078,-53 612.084,-53 612.084,-17 512.5078,-17"/>
<text text-anchor="middle" x="562.2959" y="-30.8" font-family="Times,serif" font-size="14.00" fill="#ff8c00">invite manager</text>
<path fill="none" stroke="#ff8c00" d="M604.9092,-17C604.9092,-17 695.7227,-17 695.7227,-17 701.7227,-17 707.7227,-23 707.7227,-29 707.7227,-29 707.7227,-41 707.7227,-41 707.7227,-47 701.7227,-53 695.7227,-53 695.7227,-53 604.9092,-53 604.9092,-53 598.9092,-53 592.9092,-47 592.9092,-41 592.9092,-41 592.9092,-29 592.9092,-29 592.9092,-23 598.9092,-17 604.9092,-17"/>
<text text-anchor="middle" x="650.3159" y="-30.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff8c00">invite manager</text>
</g>
<!-- unknown_user_key&#45;&gt;invite_manager -->
<g id="edge10" class="edge">
<title>unknown_user_key&#45;&gt;invite_manager</title>
<path fill="none" stroke="#ff8c00" d="M274.7912,-80.5452C338.467,-70.4579 438.7527,-54.5711 502.4793,-44.4759"/>
<polygon fill="#ff8c00" stroke="#ff8c00" points="503.0528,-47.9288 512.382,-42.9071 501.9575,-41.015 503.0528,-47.9288"/>
<text text-anchor="middle" x="455.4386" y="-31.7071" font-family="Times,serif" font-size="14.00" fill="#ff8c00">user=invite:&lt;token&gt;</text>
<path fill="none" stroke="#ff8c00" d="M334.0291,-77.6434C407.9842,-67.6724 523.7263,-52.0674 592.789,-42.7561"/>
<text text-anchor="middle" x="463.3062" y="-74.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff8c00">user=invite:&lt;token&gt;</text>
</g>
<!-- unknown_user_key&#45;&gt;err_and_exit -->
<g id="edge13" class="edge">
<title>unknown_user_key&#45;&gt;err_and_exit</title>
<path fill="none" stroke="#ff0000" d="M274.4978,-89.2935C352.2933,-89.0083 492.8294,-90.6942 612.084,-104 628.7169,-105.8558 632.5001,-108.7473 649.084,-111 682.1267,-115.4884 719.327,-118.6586 749.132,-120.7442"/>
<polygon fill="#ff0000" stroke="#ff0000" points="749.133,-124.2522 759.347,-121.437 749.6068,-117.2683 749.133,-124.2522"/>
<text text-anchor="middle" x="562.2959" y="-106.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">any other user</text>
<path fill="none" stroke="#ff0000" d="M333.7181,-89.2908C439.591,-92.8626 637.1209,-99.7853 707.7227,-104 724.1917,-104.9832 728.2588,-105.9333 744.7227,-107 789.6129,-109.9084 841.4427,-112.2584 874.8164,-113.641"/>
<text text-anchor="middle" x="650.3159" y="-106.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff0000">any other user</text>
</g>
<!-- learn_key -->
<g id="node8" class="node">
<title>learn_key</title>
<polygon fill="none" stroke="#006400" points="771.4272,-17 771.4272,-53 839.1206,-53 839.1206,-17 771.4272,-17"/>
<text text-anchor="middle" x="805.2739" y="-30.8" font-family="Times,serif" font-size="14.00" fill="#006400">learn key</text>
<path fill="none" stroke="#006400" d="M884.3911,-17C884.3911,-17 934.7427,-17 934.7427,-17 940.7427,-17 946.7427,-23 946.7427,-29 946.7427,-29 946.7427,-41 946.7427,-41 946.7427,-47 940.7427,-53 934.7427,-53 934.7427,-53 884.3911,-53 884.3911,-53 878.3911,-53 872.3911,-47 872.3911,-41 872.3911,-41 872.3911,-29 872.3911,-29 872.3911,-23 878.3911,-17 884.3911,-17"/>
<text text-anchor="middle" x="909.5669" y="-37.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">learn the</text>
<text text-anchor="middle" x="909.5669" y="-23.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">pub key</text>
</g>
<!-- invite_manager&#45;&gt;learn_key -->
<g id="edge8" class="edge">
<title>invite_manager&#45;&gt;learn_key</title>
<path fill="none" stroke="#006400" d="M612.3465,-35C656.1463,-35 719.1598,-35 761.1155,-35"/>
<polygon fill="#006400" stroke="#006400" points="761.3041,-38.5001 771.3041,-35 761.304,-31.5001 761.3041,-38.5001"/>
<text text-anchor="middle" x="685.8271" y="-37.8" font-family="Times,serif" font-size="14.00" fill="#006400">valid token</text>
<path fill="none" stroke="#006400" d="M707.8521,-35C757.9748,-35 829.1828,-35 872.2155,-35"/>
<text text-anchor="middle" x="785.1982" y="-37.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#006400">valid token</text>
</g>
<!-- invite_manager&#45;&gt;err_and_exit -->
<g id="edge14" class="edge">
<title>invite_manager&#45;&gt;err_and_exit</title>
<path fill="none" stroke="#ff0000" d="M611.4661,-53.0105C651.6045,-67.7127 708.3017,-88.4802 750.0066,-103.7562"/>
<polygon fill="#ff0000" stroke="#ff0000" points="748.8708,-107.0676 759.4646,-107.2206 751.2785,-100.4946 748.8708,-107.0676"/>
<text text-anchor="middle" x="685.8271" y="-95.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">invalid token</text>
<path fill="none" stroke="#ff0000" d="M707.8521,-52.7546C759.019,-68.5437 832.1589,-91.1133 874.868,-104.2926"/>
<text text-anchor="middle" x="785.1982" y="-91.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff0000">invalid token</text>
</g>
<!-- ssh_proxy -->
<g id="node7" class="node">
<title>ssh_proxy</title>
<polygon fill="none" stroke="#006400" points="766.3516,-168 766.3516,-204 844.1963,-204 844.1963,-168 766.3516,-168"/>
<text text-anchor="middle" x="805.2739" y="-181.8" font-family="Times,serif" font-size="14.00" fill="#006400">SSH proxy</text>
<path fill="none" stroke="#006400" d="M877.0117,-168C877.0117,-168 942.1221,-168 942.1221,-168 948.1221,-168 954.1221,-174 954.1221,-180 954.1221,-180 954.1221,-192 954.1221,-192 954.1221,-198 948.1221,-204 942.1221,-204 942.1221,-204 877.0117,-204 877.0117,-204 871.0117,-204 865.0117,-198 865.0117,-192 865.0117,-192 865.0117,-180 865.0117,-180 865.0117,-174 871.0117,-168 877.0117,-168"/>
<text text-anchor="middle" x="909.5669" y="-188.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">SSH proxy</text>
<text text-anchor="middle" x="909.5669" y="-174.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">Jump&#45;Host</text>
</g>
<!-- acl_manager&#45;&gt;ssh_proxy -->
<g id="edge7" class="edge">
<title>acl_manager&#45;&gt;ssh_proxy</title>
<path fill="none" stroke="#006400" d="M610.0008,-192.3563C641.8818,-193.0022 684.7518,-193.37 722.5703,-192 733.3636,-191.609 744.9337,-190.9319 755.8983,-190.1699"/>
<polygon fill="#006400" stroke="#006400" points="756.4612,-193.6382 766.18,-189.4199 755.9519,-186.6568 756.4612,-193.6382"/>
<text text-anchor="middle" x="685.8271" y="-194.8" font-family="Times,serif" font-size="14.00" fill="#006400">authorized</text>
<path fill="none" stroke="#006400" d="M704.0566,-192.4569C738.7694,-193.1138 784.9041,-193.4561 825.6738,-192 838.3694,-191.5466 852.1251,-190.7084 864.7541,-189.7993"/>
<text text-anchor="middle" x="785.1982" y="-195.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#006400">authorized</text>
</g>
<!-- acl_manager&#45;&gt;err_and_exit -->
<g id="edge12" class="edge">
<title>acl_manager&#45;&gt;err_and_exit</title>
<path fill="none" stroke="#ff0000" d="M610.264,-178.009C646.3866,-168.197 697.1155,-154.3556 741.5703,-142 744.1794,-141.2748 746.8478,-140.5307 749.5426,-139.7772"/>
<polygon fill="#ff0000" stroke="#ff0000" points="750.6733,-143.0952 759.3567,-137.025 748.7831,-136.3552 750.6733,-143.0952"/>
<text text-anchor="middle" x="685.8271" y="-169.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">unauthorized</text>
<path fill="none" stroke="#ff0000" d="M703.7163,-179.7682C743.1076,-170.9461 797.7781,-157.5732 844.6738,-142 854.6331,-138.6927 865.2245,-134.5604 874.8992,-130.5307"/>
<text text-anchor="middle" x="785.1982" y="-172.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff0000">unauthorized</text>
</g>
<!-- host_1 -->
<g id="node10" class="node">
<title>host_1</title>
<polygon fill="none" stroke="#0000ff" points="904.3086,-223 904.3086,-259 986.0908,-259 986.0908,-223 904.3086,-223"/>
<text text-anchor="middle" x="945.1997" y="-236.8" font-family="Times,serif" font-size="14.00" fill="#0000ff">root@host1</text>
<path fill="none" stroke="#0000ff" d="M1024.5425,-223C1024.5425,-223 1094.7456,-223 1094.7456,-223 1100.7456,-223 1106.7456,-229 1106.7456,-235 1106.7456,-235 1106.7456,-247 1106.7456,-247 1106.7456,-253 1100.7456,-259 1094.7456,-259 1094.7456,-259 1024.5425,-259 1024.5425,-259 1018.5425,-259 1012.5425,-253 1012.5425,-247 1012.5425,-247 1012.5425,-235 1012.5425,-235 1012.5425,-229 1018.5425,-223 1024.5425,-223"/>
<text text-anchor="middle" x="1059.644" y="-236.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#0000ff">root@host1</text>
</g>
<!-- ssh_proxy&#45;&gt;host_1 -->
<g id="edge3" class="edge">
<title>ssh_proxy&#45;&gt;host_1</title>
<path fill="none" stroke="#0000ff" d="M844.2511,-201.3206C859.7986,-207.4318 877.9046,-214.5486 894.4551,-221.054"/>
<polygon fill="#0000ff" stroke="#0000ff" points="893.4017,-224.4006 903.9889,-224.8015 895.9624,-217.8858 893.4017,-224.4006"/>
<path fill="none" stroke="#0000ff" d="M954.5012,-202.6151C964.678,-206.3678 975.4382,-210.3275 985.46,-214 994.2108,-217.2067 1003.5469,-220.6149 1012.54,-223.8913"/>
</g>
<!-- host_2 -->
<g id="node11" class="node">
<title>host_2</title>
<polygon fill="none" stroke="#0000ff" points="887.9775,-168 887.9775,-204 1002.4219,-204 1002.4219,-168 887.9775,-168"/>
<text text-anchor="middle" x="945.1997" y="-181.8" font-family="Times,serif" font-size="14.00" fill="#0000ff">user@host2:2222</text>
<path fill="none" stroke="#0000ff" d="M1005.46,-168C1005.46,-168 1113.8281,-168 1113.8281,-168 1119.8281,-168 1125.8281,-174 1125.8281,-180 1125.8281,-180 1125.8281,-192 1125.8281,-192 1125.8281,-198 1119.8281,-204 1113.8281,-204 1113.8281,-204 1005.46,-204 1005.46,-204 999.46,-204 993.46,-198 993.46,-192 993.46,-192 993.46,-180 993.46,-180 993.46,-174 999.46,-168 1005.46,-168"/>
<text text-anchor="middle" x="1059.644" y="-181.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#0000ff">user@host2:2222</text>
</g>
<!-- ssh_proxy&#45;&gt;host_2 -->
<g id="edge4" class="edge">
<title>ssh_proxy&#45;&gt;host_2</title>
<path fill="none" stroke="#0000ff" d="M844.2511,-186C854.6959,-186 866.2954,-186 877.8023,-186"/>
<polygon fill="#0000ff" stroke="#0000ff" points="877.8592,-189.5001 887.8591,-186 877.8591,-182.5001 877.8592,-189.5001"/>
<path fill="none" stroke="#0000ff" d="M954.1887,-186C966.458,-186 980.0332,-186 993.2463,-186"/>
</g>
<!-- host_3 -->
<g id="node12" class="node">
<title>host_3</title>
<polygon fill="none" stroke="#0000ff" points="888.3638,-113 888.3638,-149 1002.0356,-149 1002.0356,-113 888.3638,-113"/>
<text text-anchor="middle" x="945.1997" y="-126.8" font-family="Times,serif" font-size="14.00" fill="#0000ff">root@host3:1234</text>
<path fill="none" stroke="#0000ff" d="M1006.6392,-113C1006.6392,-113 1112.6489,-113 1112.6489,-113 1118.6489,-113 1124.6489,-119 1124.6489,-125 1124.6489,-125 1124.6489,-137 1124.6489,-137 1124.6489,-143 1118.6489,-149 1112.6489,-149 1112.6489,-149 1006.6392,-149 1006.6392,-149 1000.6392,-149 994.6392,-143 994.6392,-137 994.6392,-137 994.6392,-125 994.6392,-125 994.6392,-119 1000.6392,-113 1006.6392,-113"/>
<text text-anchor="middle" x="1059.644" y="-126.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#0000ff">root@host3:1234</text>
</g>
<!-- ssh_proxy&#45;&gt;host_3 -->
<g id="edge5" class="edge">
<title>ssh_proxy&#45;&gt;host_3</title>
<path fill="none" stroke="#0000ff" d="M844.2511,-170.6794C858.381,-165.1255 874.624,-158.7409 889.8921,-152.7395"/>
<polygon fill="#0000ff" stroke="#0000ff" points="891.2185,-155.9789 899.245,-149.0632 888.6578,-149.4641 891.2185,-155.9789"/>
<path fill="none" stroke="#0000ff" d="M954.1887,-169.6471C971.9014,-163.1558 992.3359,-155.667 1010.4731,-149.0201"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
.assets/overview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,7 +1,18 @@
defaults: &defaults
working_directory: /go/src/github.com/moul/sshportal
working_directory: /go/src/moul.io/sshportal
docker:
- image: circleci/golang:1.8
- image: circleci/golang:1.14.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:
@@ -9,13 +20,14 @@ jobs:
<<: *defaults
steps:
- checkout
- run: make install
- run: go get -v -t .
- run: make test
# - run: make integration
- run: go get -u github.com/alecthomas/gometalinter
- run: gometalinter --install
- run: make lint
- *install_retry
- run: /tmp/retry -m 3 go mod download
- run: /tmp/retry -m 3 go mod vendor
- run: /tmp/retry -m 3 make install
- run: GO111MODULE=off /tmp/retry -m 3 go test -v ./...
- run: /tmp/retry -m 3 curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.12.2
- run: PATH=$PATH:$(pwd)/bin /tmp/retry -m 3 make lint
docker.integration:
<<: *defaults
steps:
@@ -24,11 +36,12 @@ jobs:
name: Install Docker Compose
command: |
umask 022
curl -L https://github.com/docker/compose/releases/download/1.11.2/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
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
- run: docker build -t moul/sshportal .
- run: make integration
- *install_retry
- run: /tmp/retry -m 3 docker build -t moul/sshportal .
- run: /tmp/retry -m 3 make integration
workflows:

View File

@@ -1,3 +1,5 @@
# .git/ # should be kept for git-based versionning
examples/
.circleci/
.assets/

6
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
github: ["moul"]
patreon: moul
open_collective: sshportal
custom:
- "https://www.buymeacoffee.com/moul"
- "https://manfred.life/donate"

13
.github/workflows/release.yml vendored Normal file
View 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 }}

5
.gitignore vendored
View File

@@ -1,3 +1,8 @@
dist/
*~
*#
.*#
.DS_Store
/log/
/sshportal
*.db

34
.golangci.yml Normal file
View File

@@ -0,0 +1,34 @@
run:
deadline: 1m
tests: false
#skip-files:
# - ".*\\.gen\\.go"
linters-settings:
golint:
min-confidence: 0
maligned:
suggest-new: true
goconst:
min-len: 5
min-occurrences: 4
misspell:
locale: US
linters:
disable-all: true
enable:
- goconst
- misspell
- deadcode
- misspell
- structcheck
- errcheck
- unused
- varcheck
- staticcheck
- unconvert
- gofmt
- goimports
- golint
- ineffassign

29
.goreleaser.yml Normal file
View 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
View File

@@ -0,0 +1,8 @@
module.exports = {
branch: 'master',
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
'@semantic-release/github',
],
};

View File

@@ -4,6 +4,33 @@
* No entry
## v1.10.0 (2019-06-24)
* Bump deps, now using github.com/gliderlabs/ssh upstream
* Fix Windows build ([#101](https://github.com/moul/sshportal/pull/101)) by [@Raerten](https://github.com/Raerten)
* Use environment variables for settings ([#98](https://github.com/moul/sshportal/pull/98)) by [@Raerten](https://github.com/Raerten)
* Fix 'userkey create' ([#111](https://github.com/moul/sshportal/pull/111)) by [@shawn111](https://github.com/shawn111)
* Set log files mode to 440 instead of 640 ([#134](https://github.com/moul/sshportal/pull/134)) by [@jle64](https://github.com/jle64)
* Allow to create a host using an IP as name ([#135](https://github.com/moul/sshportal/pull/135)) by [@jle64](https://github.com/jle64)
* Add username and session ID to session log filename ([#133](https://github.com/moul/sshportal/pull/133)) by [@jle64](https://github.com/jle64)
* Unable to use encrypted SSH private keys ([#124](https://github.com/moul/sshportal/pull/124)) by [@welderpb](https://github.com/welderpb)
* Fix format of ID in new session + closing channel if host is unreachable ([#123](https://github.com/moul/sshportal/pull/123)) by [@vdaviot](https://github.com/vdaviot)
* Refactor the main package with a focus on splitting up into packages ([#113](https://github.com/moul/sshportal/pull/113)) by [@ahamidullah](https://github.com/ahamidullah)
## v1.9.0 (2018-11-18)
* Add `hostgroup update` and `usergroup update` commands ([#58](https://github.com/moul/sshportal/pull/58)) by [@adyxax](https://github.com/adyxax)
* Add socket timeout ([#80](https://github.com/moul/sshportal/pull/80)) by [@ahhx](https://github.com/ahhx)
* Add a flag to list only active sessions ([#76](https://github.com/moul/sshportal/pull/76)) by [@vdaviot](https://github.com/vdaviot)
* Unset hop on host ([#74](https://github.com/moul/sshportal/pull/74)) by [@vdaviot](https://github.com/vdaviot)
* Fix session status and duration display ([#75](https://github.com/moul/sshportal/pull/75)) by [@vdaviot](https://github.com/vdaviot)
* Fix log path and filename on Windows ([#78](https://github.com/moul/sshportal/pull/78)) by [@Raerten](https://github.com/Raerten)
* Admin user is not editable ([#69](https://github.com/moul/sshportal/pull/69)) by [@alenn-m](https://github.com/alenn-m)
* Switch to go modules (go1.11) ([#83](https://github.com/moul/sshportal/pull/83))
* Switch to moul.io/sshportal canonical URL ([#86](https://github.com/moul/sshportal/pull/86))
* Switch to golangci-lint ([#87](https://github.com/moul/sshportal/pull/87))
## v1.8.0 (2018-04-02)
* The default created user now has the same username as the user starting sshportal (was hardcoded "admin")

View File

@@ -1,7 +1,10 @@
# build
FROM golang:1.9 as builder
COPY . /go/src/github.com/moul/sshportal
WORKDIR /go/src/github.com/moul/sshportal
FROM golang:1.14.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

View File

@@ -4,10 +4,11 @@ GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
LDFLAGS ?= -X main.GitSha=$(GIT_SHA) -X main.GitTag=$(GIT_TAG) -X main.GitBranch=$(GIT_BRANCH)
VERSION ?= $(shell grep 'VERSION =' main.go | cut -d'"' -f2)
AES_KEY ?= my-dummy-aes-key
GO ?= GO111MODULE=on go
.PHONY: install
install:
go install -v -ldflags '$(LDFLAGS)' .
$(GO) install -v -ldflags '$(LDFLAGS)' .
.PHONY: docker.build
docker.build:
@@ -19,21 +20,21 @@ integration:
.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
-$(GO) get github.com/githubnemo/CompileDaemon
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal server --debug --bind-address=:$(PORT) --aes-key=$(AES_KEY) $(EXTRA_RUN_OPTS)" .
.PHONY: test
test:
go test -i .
go test -v .
$(GO) test -i ./...
$(GO) test -v ./...
.PHONY: lint
lint:
gometalinter --disable-all --enable=errcheck --enable=vet --enable=vetshadow --enable=golint --enable=gas --enable=ineffassign --enable=goconst --enable=goimports --enable=gofmt --exclude="should have comment" --enable=staticcheck --enable=gosimple --enable=misspell --deadline=60s .
golangci-lint run --verbose ./...
.PHONY: backup
backup:
@@ -44,3 +45,14 @@ 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

420
README.md
View File

@@ -1,65 +1,43 @@
# sshportal
[![CircleCI](https://circleci.com/gh/moul/sshportal.svg?style=svg)](https://circleci.com/gh/moul/sshportal)
[![Docker Build Status](https://img.shields.io/docker/build/moul/sshportal.svg)](https://hub.docker.com/r/moul/sshportal/)
[![Go Report Card](https://goreportcard.com/badge/github.com/moul/sshportal)](https://goreportcard.com/report/github.com/moul/sshportal)
[![GoDoc](https://godoc.org/github.com/moul/sshportal?status.svg)](https://godoc.org/github.com/moul/sshportal)
[![License](https://img.shields.io/github/license/moul/sshportal.svg)](https://github.com/moul/sshportal/blob/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/moul.io/sshportal)](https://goreportcard.com/report/moul.io/sshportal)
[![GoDoc](https://godoc.org/moul.io/sshportal?status.svg)](https://godoc.org/moul.io/sshportal)
[![Financial Contributors on Open Collective](https://opencollective.com/sshportal/all/badge.svg?label=financial+contributors)](https://opencollective.com/sshportal) [![License](https://img.shields.io/github/license/moul/sshportal.svg)](https://github.com/moul/sshportal/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/release/moul/sshportal.svg)](https://github.com/moul/sshportal/releases)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmoul%2Fsshportal.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmoul%2Fsshportal?ref=badge_shield)
<!-- temporarily broken? [![Docker Build Status](https://img.shields.io/docker/build/moul/sshportal.svg)](https://hub.docker.com/r/moul/sshportal/) -->
Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
![sshportal demo](https://github.com/moul/sshportal/raw/master/.assets/demo.gif)
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.
![Flow Diagram](https://raw.githubusercontent.com/moul/sshportal/master/.assets/flow-diagram.png)
---
## Overview
## Contents
![sshportal overview](https://raw.github.com/moul/sshportal/master/.assets/overview.svg?sanitize=true)
<!-- toc -->
## Features
- [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)
* Single autonomous binary (~10-20Mb) with no runtime dependencies (embeds ssh server and client)
* Portable / Cross-platform (regularly tested on linux and OSX/darwin)
* Store data in [Sqlite3](https://www.sqlite.org/) or [MySQL](https://www.mysql.com) (probably easy to add postgres, mssql thanks to gorm)
* Stateless -> horizontally scalable when using [MySQL](https://www.mysql.com) as the backend
* Connect to remote host using key or password
* Admin commands can be run directly or in an interactive shell
* Host management
* User management (invite, group, stats)
* Host Key management (create, remove, update, import)
* Automatic remote host key learning
* User Key management (multile keys per user)
* ACL management (acl+user-groups+host-groups)
* User roles (admin, trusted, standard, ...)
* User invitations (no more "give me your public ssh key please")
* Easy server installation (generate shell command to setup `authorized_keys`)
* Sensitive data encryption
* Session management (see active connections, history, stats, stop)
* Audit log (logging every user action)
* Record TTY Session
* Tunnels logging
* Host Keys verifications shared across users
* Healthcheck user (replying OK to any user)
* SSH compatibility
* ipv4 and ipv6 support
* [`scp`](https://linux.die.net/man/1/scp) support
* [`rsync`](https://linux.die.net/man/1/rsync) support
* [tunneling](https://www.ssh.com/ssh/tunneling/example) (local forward, remote forward, dynamic forward) support
* [`sftp`](https://www.ssh.com/ssh/sftp/) support
* [`ssh-agent`](https://www.ssh.com/ssh/agent) support
* [`X11 forwarding`](http://en.tldp.org/HOWTO/XDMCP-HOWTO/ssh.html) support
* Git support (can be used to easily use multiple user keys on GitHub, or access your own firewalled gitlab server)
* Do not require any SSH client modification or custom `.ssh/config`, works with every tested SSH programming libraries and every tested SSH clients
* SSH to non-SSH proxy
* [Telnet](https://www.ssh.com/ssh/telnet) support
<!-- tocstop -->
## (Known) limitations
---
* Does not work (yet?) with [`mosh`](https://mosh.org/)
## Usage
## Installation and usage
Start the server
@@ -139,9 +117,151 @@ To associate this account with a key, use the following SSH user: 'invite:NfHK5a
config>
```
## Flow Diagram
Demo gif:
![sshportal demo](https://github.com/moul/sshportal/raw/master/.assets/demo.gif)
![Flow Diagram](https://raw.github.com/moul/sshportal/master/.assets/flow-diagram.svg?sanitize=true)
---
## Use cases
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.
*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.
There are companies who use a jump host to monitor connections at a single point.
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).
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.
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.
---
## Features and limitations
* 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
**(Known) limitations**
* Does not work (yet?) with [`mosh`](https://mosh.org/)
---
## Docker
Docker is the recommended way to run sshportal.
An [automated build is setup on the Docker Hub](https://hub.docker.com/r/moul/sshportal/tags/).
```console
# Start a server in background
# mount `pwd` to persist the sqlite database file
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.10.0
# check logs (mandatory on first run to get the administrator invite token)
docker logs -f sshportal
```
The easier way to upgrade sshportal is to do the following:
```sh
# we consider you were using an old version and you want to use the new version v1.10.0
# stop and rename the last working container + backup the database
docker stop sshportal
docker rename sshportal sshportal_old
cp sshportal.db sshportal.db.bkp
# run the new version
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.10.0
# check the logs for migration or cross-version incompabitility errors
docker logs -f sshportal
```
Now you can test ssh-ing to sshportal to check if everything looks OK.
In case of problem, you can rollback to the latest working version with the latest working backup, using:
```sh
docker stop sshportal
docker rm sshportal
cp sshportal.db.bkp sshportal.db
docker rename sshportal_old sshportal
docker start sshportal
docker logs -f sshportal
```
---
## Manual Install
Get the latest version using GO.
```sh
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 portal config backup > sshportal.bkp
# Restore
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).
I suggest you to be careful during this development phase, and use an additional backup method, for example:
```sh
# sqlite dump
sqlite3 sshportal.db .dump > sshportal.sql.bkp
# or just the immortal cp
cp sshportal.db sshportal.db.bkp
```
---
## built-in shell
@@ -157,7 +277,29 @@ ssh admin@portal.example.org host inspect toto
You can enter in interactive mode using this syntax: `ssh admin@portal.example.org`
### Synopsis
![sshportal overview](https://raw.github.com/moul/sshportal/master/.assets/overview.png)
---
## 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
@@ -230,120 +372,7 @@ info [-h]
version [-h]
```
## Docker
Docker is the recommended way to run sshportal.
An [automated build is setup on the Docker Hub](https://hub.docker.com/r/moul/sshportal/tags/).
```console
# Start a server in background
# mount `pwd` to persist the sqlite database file
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.8.0
# check logs (mandatory on first run to get the administrator invite token)
docker logs -f sshportal
```
The easier way to upgrade sshportal is to do the following:
```sh
# we consider you were using an old version and you want to use the new version v1.8.0
# stop and rename the last working container + backup the database
docker stop sshportal
docker rename sshportal sshportal_old
cp sshportal.db sshportal.db.bkp
# run the new version
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.8.0
# check the logs for migration or cross-version incompabitility errors
docker logs -f sshportal
```
Now you can test ssh-ing to sshportal to check if everything looks OK.
In case of problem, you can rollback to the latest working version with the latest working backup, using:
```sh
docker stop sshportal
docker rm sshportal
cp sshportal.db.bkp sshportal.db
docker rename sshportal_old sshportal
docker start sshportal
docker logs -f sshportal
```
## Manual Install
Get the latest version using GO.
```sh
go get -u github.com/moul/sshportal
```
## portal alias (.ssh/config)
Edit your `~/.ssh/config` file (create it first if needed)
```ini
Host portal
User admin
Port 2222 # portal port
HostName 127.0.0.1 # portal hostname
```
```bash
# you can now run a shell using this:
ssh portal
# instead of this:
ssh localhost -p 2222 -l admin
# or connect to hosts using this:
ssh hostname@portal
# instead of this:
ssh localhost -p 2222 -l hostname
```
## Backup / Restore
sshportal embeds built-in backup/restore methods which basically import/export JSON objects:
```sh
# Backup
ssh portal config backup > sshportal.bkp
# Restore
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).
I suggest you to be careful during this development phase, and use an additional backup method, for example:
```sh
# sqlite dump
sqlite3 sshportal.db .dump > sshportal.sql.bkp
# or just the immortal cp
cp sshportal.db sshportal.db.bkp
```
## Demo data
The following servers are freely available, without external registration,
it makes it easier to quickly test `sshportal` without configuring your own servers to accept sshportal connections.
```
ssh portal host create new@sdf.org
ssh sdf@portal
ssh portal host create test@whoami.filippo.io
ssh whoami@portal
ssh portal host create test@chat.shazow.net
ssh chat@portal
```
---
## Healthcheck
@@ -377,6 +406,33 @@ $ 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.
@@ -385,10 +441,12 @@ By default, `sshportal` uses a local [sqlite](https://www.sqlite.org/) database
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`.
![sshportal cluster with MySQL backend](https://raw.github.com/moul/sshportal/master/.assets/cluster-mysql.svg?sanitize=true)
![sshportal cluster with MySQL backend](https://raw.github.com/moul/sshportal/master/.assets/cluster-mysql.png)
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)
@@ -406,12 +464,34 @@ See [examples/mysql](http://github.com/moul/sshportal/tree/master/examples/mysql
* https://github.com/mgutz/ansi: Terminal color helpers
* https://github.com/urfave/cli: CLI flag parsing with subcommands support
![sshportal data model](https://raw.github.com/moul/sshportal/master/.assets/sql-schema.svg?sanitize=true)
![sshportal data model](https://raw.github.com/moul/sshportal/master/.assets/sql-schema.png)
## Note
## Contributors
This is totally experimental for now, so please file issues to let me know what you think about it!
### 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>
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmoul%2Fsshportal.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmoul%2Fsshportal?ref=badge_large)
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/sshportal/contribute)]
#### Individuals
<a href="https://opencollective.com/sshportal"><img src="https://opencollective.com/sshportal/individuals.svg?width=890"></a>
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/sshportal/contribute)]
<a href="https://opencollective.com/sshportal/organization/0/website"><img src="https://opencollective.com/sshportal/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/1/website"><img src="https://opencollective.com/sshportal/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/2/website"><img src="https://opencollective.com/sshportal/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/3/website"><img src="https://opencollective.com/sshportal/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/4/website"><img src="https://opencollective.com/sshportal/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/5/website"><img src="https://opencollective.com/sshportal/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/6/website"><img src="https://opencollective.com/sshportal/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/7/website"><img src="https://opencollective.com/sshportal/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/8/website"><img src="https://opencollective.com/sshportal/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/9/website"><img src="https://opencollective.com/sshportal/organization/9/avatar.svg"></a>

1
_config.yml Normal file
View File

@@ -0,0 +1 @@
theme: jekyll-theme-slate

View File

@@ -1,47 +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 func() {
So(os.RemoveAll(tempDir), ShouldBeNil)
}()
// create sqlite db
db, err := gorm.Open("sqlite3", filepath.Join(tempDir, "sshportal.db"))
So(err, ShouldBeNil)
db.LogMode(false)
So(dbInit(db), ShouldBeNil)
// create dummy objects
var hostGroup HostGroup
err = HostGroupsByIdentifiers(db, []string{"default"}).First(&hostGroup).Error
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, ACLActionAllow)
})
}

View File

@@ -1,52 +0,0 @@
package main
import (
"fmt"
"os"
"time"
"github.com/urfave/cli"
)
type configServe struct {
aesKey string
dbDriver, dbURL string
logsLocation string
bindAddr string
debug, demo bool
idleTimeout time.Duration
}
func parseServeConfig(c *cli.Context) (*configServe, error) {
ret := &configServe{
aesKey: c.String("aes-key"),
dbDriver: c.String("db-driver"),
dbURL: c.String("db-conn"),
bindAddr: c.String("bind-address"),
debug: c.Bool("debug"),
demo: c.Bool("demo"),
logsLocation: c.String("logs-location"),
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
}

View File

@@ -5,3 +5,6 @@ run:
docker-compose exec sshportal /bin/sshportal healthcheck --wait --quiet
docker-compose run client /integration/_client.sh
docker-compose down
build:
docker-compose build

View File

@@ -28,7 +28,7 @@ ssh sshportal -l invite:integration
ssh sshportal -l admin hostgroup create
ssh sshportal -l admin hostgroup create --name=hg1
ssh sshportal -l admin hostgroup create --name=hg2 --comment=test
ssh sshportal -l admin usergroup inspect hg1 hg2
ssh sshportal -l admin hostgroup inspect hg1 hg2
ssh sshportal -l admin hostgroup ls
ssh sshportal -l admin usergroup create

View File

@@ -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: server --db-driver=mysql --debug --db-conn="root:root@tcp(mysql:3306)/db?charset=utf8&parseTime=true&loc=Local"
command: server
ports:
- 2222:2222

33
go.mod Normal file
View File

@@ -0,0 +1,33 @@
module moul.io/sshportal
require (
github.com/anmitsu/go-shlex v0.0.0-20200502080107-070676123096
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/creack/pty v1.1.10 // 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.0
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/jinzhu/gorm v1.9.12
github.com/kr/pty v1.1.8
github.com/mattn/go-colorable v0.1.6 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
github.com/olekukonko/tablewriter v0.0.4
github.com/reiver/go-oi v1.0.0
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 // indirect
github.com/smartystreets/goconvey v1.6.4
github.com/urfave/cli v1.22.4
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25 // indirect
gopkg.in/gormigrate.v1 v1.6.0
moul.io/srand v1.4.0
)
go 1.14

128
go.sum Normal file
View File

@@ -0,0 +1,128 @@
cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/anmitsu/go-shlex v0.0.0-20200502080107-070676123096 h1:ZruJGjP2kDYJM4UYAeWKJpWAnw4S0Xa9c5sVO3dp4B8=
github.com/anmitsu/go-shlex v0.0.0-20200502080107-070676123096/go.mod h1:yiw7E4c5EVh3s1/gBE3mE3ObvBGmvKsguqJmDKd2Vlc=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.10 h1:Xv3/hZlzZeTSMk5upBEt3iFdxWaPS3xYIm+BBySIqlY=
github.com/creack/pty v1.1.10/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.3.0 h1:7GcKy4erEljCE/QeQ2jTVpu+3f3zkpZOxOJjFYkMqYU=
github.com/gliderlabs/ssh v0.3.0/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jinzhu/gorm v1.9.2 h1:lCvgEaqe/HVE+tjAR2mt4HbbHAZsQOv3XAZiEZV37iw=
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3 h1:xvj06l8iSwiWpYgm8MbPp+naBg+pwfqmdXabzqPCn/8=
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/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.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
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 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e h1:quuzZLi72kkJjl+f5AQ93FMcadG19WkS7MO6TXFOSas=
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvEWwEIx86DB9Ke/+a5wBI464eDRo3eF0LcfpWg=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3 h1:yxUGvEatvDMO6gkhwx82Va+Czdyui9LiCw6a5YB/2f8=
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3/go.mod h1:1Q04m7wmv/IMoZU9t8UkH+n9McWn4i3H9v9LnMgqloo=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 h1:hBSHahWMEgzwRyS6dRpxY0XyjZsHyQ61s084wo5PJe0=
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 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/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli v1.22.4/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-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-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-20200511232937-7e40ca221e25 h1:OKbAoGs4fGM5cPLlVQLZGYkFC8OnOfgo6tt0Smf9XhM=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/gormigrate.v1 v1.6.0 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI=
gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
moul.io/srand v1.4.0 h1:r5ZMiWDN0ni0lTV7KzJR/jx0K7GivJYW5WaXmufgeik=
moul.io/srand v1.4.0/go.mod h1:P2uaZB+GFstFNo8sEj6/U8FRV1n25kD0LLckFpJ+qvc=

View 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
View 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

View 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 }}

View 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 -}}

View 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 }}

View 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 }}

View 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 }}

View 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
View 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

View File

@@ -1,12 +0,0 @@
// +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 supported on this architecture") }

106
main.go
View File

@@ -1,25 +1,20 @@
package main
package main // import "moul.io/sshportal"
import (
"fmt"
"log"
"math"
"math/rand"
"net"
"os"
"path"
"time"
"github.com/gliderlabs/ssh"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/urfave/cli"
"moul.io/srand"
)
var (
// Version should be updated by hand at each release
Version = "1.8.0+dev"
Version = "1.10.0+dev"
// GitTag will be overwritten automatically by the build system
GitTag string
// GitSha will be overwritten automatically by the build system
@@ -29,13 +24,13 @@ var (
)
func main() {
rand.Seed(time.Now().UnixNano())
rand.Seed(srand.Secure())
app := cli.NewApp()
app.Name = path.Base(os.Args[0])
app.Author = "Manfred Touron"
app.Version = Version + " (" + GitSha + ")"
app.Email = "https://github.com/moul/sshportal"
app.Email = "https://moul.io/sshportal"
app.Commands = []cli.Command{
{
Name: "server",
@@ -44,7 +39,7 @@ func main() {
if err := ensureLogDirectory(c.String("logs-location")); err != nil {
return err
}
cfg, err := parseServeConfig(c)
cfg, err := parseServerConfig(c)
if err != nil {
return err
}
@@ -58,27 +53,32 @@ func main() {
Usage: "SSH server bind address",
},
cli.StringFlag{
Name: "db-driver",
Value: "sqlite3",
Usage: "GORM driver (sqlite3)",
Name: "db-driver",
EnvVar: "SSHPORTAL_DB_DRIVER",
Value: "sqlite3",
Usage: "GORM driver (sqlite3)",
},
cli.StringFlag{
Name: "db-conn",
Value: "./sshportal.db",
Usage: "GORM connection string",
Name: "db-conn",
EnvVar: "SSHPORTAL_DATABASE_URL",
Value: "./sshportal.db",
Usage: "GORM connection string",
},
cli.BoolFlag{
Name: "debug, D",
Usage: "Display debug information",
Name: "debug, D",
EnvVar: "SSHPORTAL_DEBUG",
Usage: "Display debug information",
},
cli.StringFlag{
Name: "aes-key",
Usage: "Encrypt sensitive data in database (length: 16, 24 or 32)",
Name: "aes-key",
EnvVar: "SSHPORTAL_AES_KEY",
Usage: "Encrypt sensitive data in database (length: 16, 24 or 32)",
},
cli.StringFlag{
Name: "logs-location",
Value: "./log",
Usage: "Store user session files",
Name: "logs-location",
EnvVar: "SSHPORTAL_LOGS_LOCATION",
Value: "./log",
Usage: "Store user session files",
},
cli.DurationFlag{
Name: "idle-timeout",
@@ -114,61 +114,3 @@ func main() {
log.Fatalf("error: %v", err)
}
}
func server(c *configServe) (err error) {
var db = (*gorm.DB)(nil)
// try to setup the local DB
if db, err = gorm.Open(c.dbDriver, c.dbURL); err != nil {
return
}
defer func() {
origErr := err
err = db.Close()
if origErr != nil {
err = origErr
}
}()
if err = db.DB().Ping(); err != nil {
return
}
db.LogMode(c.debug)
if err = dbInit(db); err != nil {
return
}
// create TCP listening socket
ln, err := net.Listen("tcp", c.bindAddr)
if err != nil {
return err
}
// configure server
srv := &ssh.Server{
Addr: c.bindAddr,
Handler: shellHandler, // ssh.Server.Handler is the handler for the DefaultSessionHandler
Version: fmt.Sprintf("sshportal-%s", Version),
ChannelHandler: channelHandler,
}
if c.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(publicKeyAuthHandler(db, c)),
ssh.PasswordAuth(passwordAuthHandler(db, c)),
// retrieve sshportal SSH private key from database
privateKeyFromDB(db, c.aesKey),
} {
if err := srv.SetOption(opt); err != nil {
return err
}
}
log.Printf("info: SSH Server accepting connections on %s, idle-timout=%v", c.bindAddr, c.idleTimeout)
return srv.Serve(ln)
}

View File

@@ -1,16 +1,20 @@
package main
package bastion // import "moul.io/sshportal/pkg/bastion"
import "sort"
import (
"sort"
type ByWeight []*ACL
"moul.io/sshportal/pkg/dbmodels"
)
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 }
type byWeight []*dbmodels.ACL
func CheckACLs(user User, host Host) (string, error) {
func (a byWeight) Len() int { return len(a) }
func (a byWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byWeight) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
func checkACLs(user dbmodels.User, host dbmodels.Host) (string, error) {
// shared ACLs between user and host
aclMap := map[uint]*ACL{}
aclMap := map[uint]*dbmodels.ACL{}
for _, userGroup := range user.Groups {
for _, userGroupACL := range userGroup.ACLs {
for _, hostGroup := range host.Groups {
@@ -26,15 +30,15 @@ func CheckACLs(user User, host Host) (string, error) {
// deny by default if no shared ACL
if len(aclMap) == 0 {
return ACLActionDeny, nil // default action
return string(dbmodels.ACLActionDeny), nil // default action
}
// transform map to slice and sort it
acls := make([]*ACL, 0, len(aclMap))
acls := make([]*dbmodels.ACL, 0, len(aclMap))
for _, acl := range aclMap {
acls = append(acls, acl)
}
sort.Sort(ByWeight(acls))
sort.Sort(byWeight(acls))
return acls[0].Action, nil
}

50
pkg/bastion/acl_test.go Normal file
View File

@@ -0,0 +1,50 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
. "github.com/smartystreets/goconvey/convey"
"moul.io/sshportal/pkg/dbmodels"
)
func TestCheckACLs(t *testing.T) {
Convey("Testing CheckACLs", t, func(c C) {
// create tmp dir
tempDir, err := ioutil.TempDir("", "sshportal")
c.So(err, ShouldBeNil)
defer func() {
c.So(os.RemoveAll(tempDir), ShouldBeNil)
}()
// create sqlite db
db, err := gorm.Open("sqlite3", filepath.Join(tempDir, "sshportal.db"))
c.So(err, ShouldBeNil)
db.LogMode(false)
c.So(DBInit(db), ShouldBeNil)
// create dummy objects
var hostGroup dbmodels.HostGroup
err = dbmodels.HostGroupsByIdentifiers(db, []string{"default"}).First(&hostGroup).Error
c.So(err, ShouldBeNil)
db.Create(&dbmodels.Host{Groups: []*dbmodels.HostGroup{&hostGroup}})
//. load db
var (
hosts []dbmodels.Host
users []dbmodels.User
)
db.Preload("Groups").Preload("Groups.ACLs").Find(&hosts)
db.Preload("Groups").Preload("Groups.ACLs").Find(&users)
// test
action, err := checkACLs(users[0], hosts[0])
c.So(err, ShouldBeNil)
c.So(action, ShouldEqual, dbmodels.ACLActionAllow)
})
}

View File

@@ -1,20 +1,23 @@
package main
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"fmt"
"io/ioutil"
"log"
"math/rand"
"os"
"os/user"
"strings"
"time"
"github.com/go-gormigrate/gormigrate"
"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 {
func DBInit(db *gorm.DB) error {
log.SetOutput(ioutil.Discard)
db.Callback().Delete().Replace("gorm:delete", hardDeleteCallback)
log.SetOutput(os.Stderr)
@@ -43,9 +46,9 @@ func dbInit(db *gorm.DB) error {
Type string
Length uint
Fingerprint string
PrivKey string `sql:"size:10000"`
PubKey string `sql:"size:10000"`
Hosts []*Host `gorm:"ForeignKey:SSHKeyID"`
PrivKey string `sql:"size:10000"`
PubKey string `sql:"size:10000"`
Hosts []*dbmodels.Host `gorm:"ForeignKey:SSHKeyID"`
Comment string
}
return tx.AutoMigrate(&SSHKey{}).Error
@@ -63,9 +66,9 @@ func dbInit(db *gorm.DB) error {
Addr string
User string
Password string
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
Fingerprint string
Comment string
}
@@ -79,9 +82,9 @@ func dbInit(db *gorm.DB) error {
Migrate: func(tx *gorm.DB) error {
type UserKey struct {
gorm.Model
Key []byte `sql:"size:10000"`
UserID uint ``
User *User `gorm:"ForeignKey:UserID"`
Key []byte `sql:"size:10000"`
UserID uint ``
User *dbmodels.User `gorm:"ForeignKey:UserID"`
Comment string
}
return tx.AutoMigrate(&UserKey{}).Error
@@ -98,8 +101,8 @@ func dbInit(db *gorm.DB) error {
IsAdmin bool
Email string
Name string
Keys []*UserKey `gorm:"ForeignKey:UserID"`
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
Comment string
InviteToken string
}
@@ -114,8 +117,8 @@ func dbInit(db *gorm.DB) error {
type UserGroup struct {
gorm.Model
Name string
Users []*User `gorm:"many2many:user_user_groups;"`
ACLs []*ACL `gorm:"many2many:user_group_acls;"`
Users []*dbmodels.User `gorm:"many2many:user_user_groups;"`
ACLs []*dbmodels.ACL `gorm:"many2many:user_group_acls;"`
Comment string
}
return tx.AutoMigrate(&UserGroup{}).Error
@@ -129,8 +132,8 @@ func dbInit(db *gorm.DB) error {
type HostGroup struct {
gorm.Model
Name string
Hosts []*Host `gorm:"many2many:host_host_groups;"`
ACLs []*ACL `gorm:"many2many:host_group_acls;"`
Hosts []*dbmodels.Host `gorm:"many2many:host_host_groups;"`
ACLs []*dbmodels.ACL `gorm:"many2many:host_group_acls;"`
Comment string
}
return tx.AutoMigrate(&HostGroup{}).Error
@@ -143,8 +146,8 @@ func dbInit(db *gorm.DB) error {
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;"`
HostGroups []*dbmodels.HostGroup `gorm:"many2many:host_group_acls;"`
UserGroups []*dbmodels.UserGroup `gorm:"many2many:user_group_acls;"`
HostPattern string
Action string
Weight uint
@@ -159,64 +162,64 @@ func dbInit(db *gorm.DB) error {
}, {
ID: "9",
Migrate: func(tx *gorm.DB) error {
db.Model(&Setting{}).RemoveIndex("uix_settings_name")
return db.Model(&Setting{}).AddUniqueIndex("uix_settings_name", "name").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(&Setting{}).RemoveIndex("uix_settings_name").Error
return db.Model(&dbmodels.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{}).AddUniqueIndex("uix_keys_name", "name").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(&SSHKey{}).RemoveIndex("uix_keys_name").Error
return db.Model(&dbmodels.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{}).AddUniqueIndex("uix_hosts_name", "name").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(&Host{}).RemoveIndex("uix_hosts_name").Error
return db.Model(&dbmodels.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{}).AddUniqueIndex("uix_users_name", "name").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(&User{}).RemoveIndex("uix_users_name").Error
return db.Model(&dbmodels.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{}).AddUniqueIndex("uix_usergroups_name", "name").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(&UserGroup{}).RemoveIndex("uix_usergroups_name").Error
return db.Model(&dbmodels.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{}).AddUniqueIndex("uix_hostgroups_name", "name").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(&HostGroup{}).RemoveIndex("uix_hostgroups_name").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 []*User `gorm:"many2many:user_user_roles"`
Name string `valid:"required,length(1|32),unix_user"`
Users []*dbmodels.User `gorm:"many2many:user_user_roles"`
}
return tx.AutoMigrate(&UserRole{}).Error
},
@@ -229,13 +232,13 @@ func dbInit(db *gorm.DB) error {
type User struct {
gorm.Model
IsAdmin bool
Roles []*UserRole `gorm:"many2many:user_user_roles"`
Email string `valid:"required,email"`
Name string `valid:"required,length(1|32),unix_user"`
Keys []*UserKey `gorm:"ForeignKey:UserID"`
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
Comment string `valid:"optional"`
InviteToken string `valid:"optional,length(10|60)"`
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
},
@@ -245,20 +248,20 @@ func dbInit(db *gorm.DB) error {
}, {
ID: "17",
Migrate: func(tx *gorm.DB) error {
return tx.Create(&UserRole{Name: "admin"}).Error
return tx.Create(&dbmodels.UserRole{Name: "admin"}).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Where("name = ?", "admin").Delete(&UserRole{}).Error
return tx.Where("name = ?", "admin").Delete(&dbmodels.UserRole{}).Error
},
}, {
ID: "18",
Migrate: func(tx *gorm.DB) error {
var adminRole UserRole
var adminRole dbmodels.UserRole
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
return err
}
var users []User
var users []dbmodels.User
if err := db.Preload("Roles").Where("is_admin = ?", true).Find(&users).Error; err != nil {
return err
}
@@ -279,13 +282,13 @@ func dbInit(db *gorm.DB) error {
Migrate: func(tx *gorm.DB) error {
type User struct {
gorm.Model
Roles []*UserRole `gorm:"many2many:user_user_roles"`
Email string `valid:"required,email"`
Name string `valid:"required,length(1|32),unix_user"`
Keys []*UserKey `gorm:"ForeignKey:UserID"`
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
Comment string `valid:"optional"`
InviteToken string `valid:"optional,length(10|60)"`
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
},
@@ -295,24 +298,24 @@ func dbInit(db *gorm.DB) error {
}, {
ID: "20",
Migrate: func(tx *gorm.DB) error {
return tx.Create(&UserRole{Name: "listhosts"}).Error
return tx.Create(&dbmodels.UserRole{Name: "listhosts"}).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Where("name = ?", "listhosts").Delete(&UserRole{}).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 *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"`
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
},
@@ -324,12 +327,12 @@ func dbInit(db *gorm.DB) error {
Migrate: func(tx *gorm.DB) error {
type Event struct {
gorm.Model
Author *User `gorm:"ForeignKey:AuthorID"`
AuthorID uint `valid:"optional"`
Domain string `valid:"required"`
Action string `valid:"required"`
Entity string `valid:"optional"`
Args []byte `sql:"size:10000" valid:"optional,length(1|10000)"`
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
},
@@ -341,11 +344,11 @@ func dbInit(db *gorm.DB) error {
Migrate: func(tx *gorm.DB) error {
type UserKey struct {
gorm.Model
Key []byte `sql:"size:10000" valid:"required,length(1|10000)"`
AuthorizedKey string `sql:"size:10000" valid:"required,length(1|10000)"`
UserID uint ``
User *User `gorm:"ForeignKey:UserID"`
Comment string `valid:"optional"`
Key []byte `sql:"size:10000" valid:"required,length(1|10000)"`
AuthorizedKey string `sql:"size:10000" valid:"required,length(1|10000)"`
UserID uint ``
User *dbmodels.User `gorm:"ForeignKey:UserID"`
Comment string `valid:"optional"`
}
return tx.AutoMigrate(&UserKey{}).Error
},
@@ -355,7 +358,7 @@ func dbInit(db *gorm.DB) error {
}, {
ID: "24",
Migrate: func(tx *gorm.DB) error {
var userKeys []UserKey
var userKeys []dbmodels.UserKey
if err := db.Find(&userKeys).Error; err != nil {
return err
}
@@ -381,16 +384,16 @@ func dbInit(db *gorm.DB) error {
type Host struct {
// FIXME: use uuid for ID
gorm.Model
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
Addr string `valid:"required"`
User string `valid:"optional"`
Password string `valid:"optional"`
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000" valid:"optional"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
Fingerprint string `valid:"optional"` // FIXME: replace with hostKey ?
Comment string `valid:"optional"`
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
Addr string `valid:"required"`
User string `valid:"optional"`
Password string `valid:"optional"`
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000" valid:"optional"`
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
Fingerprint string `valid:"optional"` // FIXME: replace with hostKey ?
Comment string `valid:"optional"`
}
return tx.AutoMigrate(&Host{}).Error
},
@@ -402,14 +405,14 @@ func dbInit(db *gorm.DB) error {
Migrate: func(tx *gorm.DB) error {
type Session struct {
gorm.Model
StoppedAt *time.Time `sql:"index" valid:"optional"`
Status string `valid:"required"`
User *User `gorm:"ForeignKey:UserID"`
Host *Host `gorm:"ForeignKey:HostID"`
UserID uint `valid:"optional"`
HostID uint `valid:"optional"`
ErrMsg string `valid:"optional"`
Comment string `valid:"optional"`
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
},
@@ -419,7 +422,7 @@ func dbInit(db *gorm.DB) error {
}, {
ID: "27",
Migrate: func(tx *gorm.DB) error {
var sessions []Session
var sessions []dbmodels.Session
if err := db.Find(&sessions).Error; err != nil {
return err
}
@@ -447,10 +450,10 @@ func dbInit(db *gorm.DB) error {
User string
Password string
URL string
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000"`
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
Comment string
}
return tx.AutoMigrate(&Host{}).Error
@@ -469,12 +472,12 @@ func dbInit(db *gorm.DB) error {
User string
Password string
URL string
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
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 *Host
Hop *dbmodels.Host
HopID uint
}
return tx.AutoMigrate(&Host{}).Error
@@ -487,7 +490,7 @@ func dbInit(db *gorm.DB) error {
if err := m.Migrate(); err != nil {
return err
}
NewEvent("system", "migrated").Log(db)
dbmodels.NewEvent("system", "migrated").Log(db)
// create default ssh key
var count uint
@@ -495,7 +498,7 @@ func dbInit(db *gorm.DB) error {
return err
}
if count == 0 {
key, err := NewSSHKey("rsa", 2048)
key, err := crypto.NewSSHKey("rsa", 2048)
if err != nil {
return err
}
@@ -511,7 +514,7 @@ func dbInit(db *gorm.DB) error {
return err
}
if count == 0 {
hostGroup := HostGroup{
hostGroup := dbmodels.HostGroup{
Name: "default",
Comment: "created by sshportal",
}
@@ -525,7 +528,7 @@ func dbInit(db *gorm.DB) error {
return err
}
if count == 0 {
userGroup := UserGroup{
userGroup := dbmodels.UserGroup{
Name: "default",
Comment: "created by sshportal",
}
@@ -539,13 +542,13 @@ func dbInit(db *gorm.DB) error {
return err
}
if count == 0 {
var defaultUserGroup UserGroup
var defaultUserGroup dbmodels.UserGroup
db.Where("name = ?", "default").First(&defaultUserGroup)
var defaultHostGroup HostGroup
var defaultHostGroup dbmodels.HostGroup
db.Where("name = ?", "default").First(&defaultHostGroup)
acl := ACL{
UserGroups: []*UserGroup{&defaultUserGroup},
HostGroups: []*HostGroup{&defaultHostGroup},
acl := dbmodels.ACL{
UserGroups: []*dbmodels.UserGroup{&defaultUserGroup},
HostGroups: []*dbmodels.HostGroup{&defaultHostGroup},
Action: "allow",
//HostPattern: "",
//Weight: 0,
@@ -557,7 +560,7 @@ func dbInit(db *gorm.DB) error {
}
// create admin user
var defaultUserGroup UserGroup
var defaultUserGroup dbmodels.UserGroup
db.Where("name = ?", "default").First(&defaultUserGroup)
if err := db.Table("users").Count(&count).Error; err != nil {
return err
@@ -568,7 +571,7 @@ func dbInit(db *gorm.DB) error {
if os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN") != "" {
inviteToken = os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN")
}
var adminRole UserRole
var adminRole dbmodels.UserRole
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
return err
}
@@ -583,13 +586,13 @@ func dbInit(db *gorm.DB) error {
if username == "" {
username = "admin" // fallback username
}
user := User{
user := dbmodels.User{
Name: username,
Email: fmt.Sprintf("%s@localhost", username),
Comment: "created by sshportal",
Roles: []*UserRole{&adminRole},
Roles: []*dbmodels.UserRole{&adminRole},
InviteToken: inviteToken,
Groups: []*UserGroup{&defaultUserGroup},
Groups: []*dbmodels.UserGroup{&defaultUserGroup},
}
if err := db.Create(&user).Error; err != nil {
return err
@@ -602,7 +605,7 @@ func dbInit(db *gorm.DB) error {
return err
}
if count == 0 {
key, err := NewSSHKey("rsa", 2048)
key, err := crypto.NewSSHKey("rsa", 2048)
if err != nil {
return err
}
@@ -614,8 +617,8 @@ func dbInit(db *gorm.DB) error {
}
// close unclosed connections
return db.Table("sessions").Where("status = ?", "active").Updates(&Session{
Status: SessionStatusClosed,
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
}
@@ -643,3 +646,13 @@ func addExtraSpaceIfExist(str string) string {
}
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
View 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()
}

View File

@@ -1,35 +1,26 @@
package bastionsession
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/arkan/bastion/pkg/logchannel"
"github.com/gliderlabs/ssh"
"github.com/moul/sshportal/pkg/logtunnel"
"github.com/sabban/bastion/pkg/logchannel"
gossh "golang.org/x/crypto/ssh"
)
type ForwardData struct {
DestinationHost string
DestinationPort uint32
SourceHost string
SourcePort uint32
}
type Config struct {
type sessionConfig struct {
Addr string
Logs string
ClientConfig *gossh.ClientConfig
}
func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []Config) error {
func multiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []sessionConfig, sessionID uint) error {
var lastClient *gossh.Client
switch newChan.ChannelType() {
case "session":
@@ -57,6 +48,7 @@ func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.
client = gossh.NewClient(ncc, chans, reqs)
}
if err != nil {
lch.Close() // fix #56
return err
}
defer func() { _ = client.Close() }()
@@ -68,8 +60,10 @@ func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.
return err
}
user := conn.User()
actx := ctx.Value(authContextKey).(*authContext)
username := actx.user.Name
// pipe everything
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, newChan)
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, username, sessionID, newChan)
case "direct-tcpip":
lch, lreqs, err := newChan.Accept()
// TODO: defer clean closer
@@ -95,13 +89,14 @@ func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.
client = gossh.NewClient(ncc, chans, reqs)
}
if err != nil {
lch.Close()
return err
}
defer func() { _ = client.Close() }()
lastClient = client
}
d := logtunnel.ForwardData{}
d := logTunnelForwardData{}
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
return err
}
@@ -110,15 +105,19 @@ func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.
return err
}
user := conn.User()
actx := ctx.Value(authContextKey).(*authContext)
username := actx.user.Name
// pipe everything
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, newChan)
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, username, sessionID, newChan)
default:
newChan.Reject(gossh.UnknownChannelType, "unsupported channel type")
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
log.Printf("failed to reject chan: %v", err)
}
return nil
}
}
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocation string, user string, newChan gossh.NewChannel) error {
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocation string, user string, username string, sessionID uint, newChan gossh.NewChannel) error {
defer func() {
_ = lch.Close()
_ = rch.Close()
@@ -126,16 +125,18 @@ func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocati
errch := make(chan error, 1)
channeltype := newChan.ChannelType()
fileNameUnix := strings.Join([]string{logsLocation, "/", user, "-", channeltype, "-", strconv.FormatInt(time.Now().UnixNano(), 10)}, "") // get user
file_name := filepath.FromSlash(fileNameUnix)
f, err := os.OpenFile(file_name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640)
defer f.Close()
filename := strings.Join([]string{logsLocation, "/", user, "-", username, "-", channeltype, "-", fmt.Sprint(sessionID), "-", time.Now().Format(time.RFC3339)}, "") // get user
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0440)
defer func() {
_ = f.Close()
}()
if err != nil {
log.Fatalf("error: %v", err)
}
log.Printf("Session %v is recorded in %v", channeltype, file_name)
log.Printf("Session %v is recorded in %v", channeltype, filename)
if channeltype == "session" {
wrappedlch := logchannel.New(lch, f)
go func() {
@@ -149,12 +150,12 @@ func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocati
}()
}
if channeltype == "direct-tcpip" {
d := logtunnel.ForwardData{}
d := logTunnelForwardData{}
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
return err
}
wrappedlch := logtunnel.New(lch, f, d.SourceHost)
wrappedrch := logtunnel.New(rch, f, d.DestinationHost)
wrappedlch := newLogTunnel(lch, f, d.SourceHost)
wrappedrch := newLogTunnel(rch, f, d.DestinationHost)
go func() {
_, _ = io.Copy(wrappedlch, rch)
errch <- errors.New("lch closed the connection")
@@ -176,7 +177,9 @@ func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocati
if req.Type == "exec" {
wrappedlch := logchannel.New(lch, f)
command := append(req.Payload, []byte("\n")...)
wrappedlch.LogWrite(command)
if _, err := wrappedlch.LogWrite(command); err != nil {
log.Printf("failed to write log: %v", err)
}
}
if err != nil {

View File

@@ -1,24 +1,29 @@
package main
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"regexp"
"runtime"
"strings"
"time"
shlex "github.com/anmitsu/go-shlex"
"github.com/asaskevich/govalidator"
"github.com/docker/docker/pkg/namesgenerator"
humanize "github.com/dustin/go-humanize"
"github.com/gliderlabs/ssh"
"github.com/mgutz/ansi"
"github.com/moby/moby/pkg/namesgenerator"
"github.com/olekukonko/tablewriter"
"github.com/urfave/cli"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
"moul.io/sshportal/pkg/crypto"
"moul.io/sshportal/pkg/dbmodels"
)
var banner = `
@@ -36,7 +41,7 @@ const (
naMessage = "n/a"
)
func shell(s ssh.Session) error {
func shell(s ssh.Session, version, gitSha, gitTag, gitBranch string) error {
var (
sshCommand = s.Command()
actx = s.Context().Value(authContextKey).(*authContext)
@@ -81,35 +86,35 @@ GLOBAL OPTIONS:
cli.StringSliceFlag{Name: "usergroup, ug", Usage: "Assigns `USERGROUP` to the acl"},
cli.StringFlag{Name: "pattern", Usage: "Assigns a host pattern to the acl"},
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
cli.StringFlag{Name: "action", Usage: "Assigns the ACL action (allow,deny)", Value: ACLActionAllow},
cli.StringFlag{Name: "action", Usage: "Assigns the ACL action (allow,deny)", Value: string(dbmodels.ACLActionAllow)},
cli.UintFlag{Name: "weight, w", Usage: "Assigns the ACL weight (priority)"},
},
Action: func(c *cli.Context) error {
if err := myself.CheckRoles([]string{"admin"}); err != nil {
return err
}
acl := ACL{
acl := dbmodels.ACL{
Comment: c.String("comment"),
HostPattern: c.String("pattern"),
UserGroups: []*UserGroup{},
HostGroups: []*HostGroup{},
UserGroups: []*dbmodels.UserGroup{},
HostGroups: []*dbmodels.HostGroup{},
Weight: c.Uint("weight"),
Action: c.String("action"),
}
if acl.Action != ACLActionAllow && acl.Action != ACLActionDeny {
if acl.Action != string(dbmodels.ACLActionAllow) && acl.Action != string(dbmodels.ACLActionDeny) {
return fmt.Errorf("invalid action %q, allowed values: allow, deny", acl.Action)
}
if _, err := govalidator.ValidateStruct(acl); err != nil {
return err
}
var userGroups []*UserGroup
if err := UserGroupsPreload(UserGroupsByIdentifiers(db, c.StringSlice("usergroup"))).Find(&userGroups).Error; err != nil {
var userGroups []*dbmodels.UserGroup
if err := dbmodels.UserGroupsPreload(dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("usergroup"))).Find(&userGroups).Error; err != nil {
return err
}
acl.UserGroups = append(acl.UserGroups, userGroups...)
var hostGroups []*HostGroup
if err := HostGroupsPreload(HostGroupsByIdentifiers(db, c.StringSlice("hostgroup"))).Find(&hostGroups).Error; err != nil {
var hostGroups []*dbmodels.HostGroup
if err := dbmodels.HostGroupsPreload(dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("hostgroup"))).Find(&hostGroups).Error; err != nil {
return err
}
acl.HostGroups = append(acl.HostGroups, hostGroups...)
@@ -139,8 +144,8 @@ GLOBAL OPTIONS:
return err
}
var acls []ACL
if err := ACLsPreload(ACLsByIdentifiers(db, c.Args())).Find(&acls).Error; err != nil {
var acls []dbmodels.ACL
if err := dbmodels.ACLsPreload(dbmodels.ACLsByIdentifiers(db, c.Args())).Find(&acls).Error; err != nil {
return err
}
@@ -160,10 +165,10 @@ GLOBAL OPTIONS:
return err
}
var acls []*ACL
var acls []*dbmodels.ACL
query := db.Order("created_at desc").Preload("UserGroups").Preload("HostGroups")
if c.Bool("latest") {
var acl ACL
var acl dbmodels.ACL
if err := query.First(&acl).Error; err != nil {
return err
}
@@ -221,7 +226,7 @@ GLOBAL OPTIONS:
return err
}
return ACLsByIdentifiers(db, c.Args()).Delete(&ACL{}).Error
return dbmodels.ACLsByIdentifiers(db, c.Args()).Delete(&dbmodels.ACL{}).Error
},
}, {
Name: "update",
@@ -245,15 +250,15 @@ GLOBAL OPTIONS:
return err
}
var acls []ACL
if err := ACLsByIdentifiers(db, c.Args()).Find(&acls).Error; err != nil {
var acls []dbmodels.ACL
if err := dbmodels.ACLsByIdentifiers(db, c.Args()).Find(&acls).Error; err != nil {
return err
}
tx := db.Begin()
for _, acl := range acls {
model := tx.Model(&acl)
update := ACL{
update := dbmodels.ACL{
Action: c.String("action"),
HostPattern: c.String("pattern"),
Weight: c.Uint("weight"),
@@ -265,13 +270,13 @@ GLOBAL OPTIONS:
}
// associations
var appendUserGroups []UserGroup
var deleteUserGroups []UserGroup
if err := UserGroupsByIdentifiers(db, c.StringSlice("assign-usergroup")).Find(&appendUserGroups).Error; err != nil {
var appendUserGroups []dbmodels.UserGroup
var deleteUserGroups []dbmodels.UserGroup
if err := dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("assign-usergroup")).Find(&appendUserGroups).Error; err != nil {
tx.Rollback()
return err
}
if err := UserGroupsByIdentifiers(db, c.StringSlice("unassign-usergroup")).Find(&deleteUserGroups).Error; err != nil {
if err := dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("unassign-usergroup")).Find(&deleteUserGroups).Error; err != nil {
tx.Rollback()
return err
}
@@ -286,13 +291,13 @@ GLOBAL OPTIONS:
}
}
var appendHostGroups []HostGroup
var deleteHostGroups []HostGroup
if err := HostGroupsByIdentifiers(db, c.StringSlice("assign-hostgroup")).Find(&appendHostGroups).Error; err != nil {
var appendHostGroups []dbmodels.HostGroup
var deleteHostGroups []dbmodels.HostGroup
if err := dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("assign-hostgroup")).Find(&appendHostGroups).Error; err != nil {
tx.Rollback()
return err
}
if err := HostGroupsByIdentifiers(db, c.StringSlice("unassign-hostgroup")).Find(&deleteHostGroups).Error; err != nil {
if err := dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("unassign-hostgroup")).Find(&deleteHostGroups).Error; err != nil {
tx.Rollback()
return err
}
@@ -330,62 +335,62 @@ GLOBAL OPTIONS:
return err
}
config := Config{}
if err := HostsPreload(db).Find(&config.Hosts).Error; err != nil {
config := dbmodels.Config{}
if err := dbmodels.HostsPreload(db).Find(&config.Hosts).Error; err != nil {
return err
}
if err := SSHKeysPreload(db).Find(&config.SSHKeys).Error; err != nil {
if err := dbmodels.SSHKeysPreload(db).Find(&config.SSHKeys).Error; err != nil {
return err
}
for _, key := range config.SSHKeys {
SSHKeyDecrypt(actx.config.aesKey, key)
crypto.SSHKeyDecrypt(actx.aesKey, key)
}
if !c.Bool("decrypt") {
for _, key := range config.SSHKeys {
if err := SSHKeyEncrypt(actx.config.aesKey, key); err != nil {
if err := crypto.SSHKeyEncrypt(actx.aesKey, key); err != nil {
return err
}
}
}
if err := HostsPreload(db).Find(&config.Hosts).Error; err != nil {
if err := dbmodels.HostsPreload(db).Find(&config.Hosts).Error; err != nil {
return err
}
for _, host := range config.Hosts {
HostDecrypt(actx.config.aesKey, host)
crypto.HostDecrypt(actx.aesKey, host)
}
if !c.Bool("decrypt") {
for _, host := range config.Hosts {
if err := HostEncrypt(actx.config.aesKey, host); err != nil {
if err := crypto.HostEncrypt(actx.aesKey, host); err != nil {
return err
}
}
}
if err := UserKeysPreload(db).Find(&config.UserKeys).Error; err != nil {
if err := dbmodels.UserKeysPreload(db).Find(&config.UserKeys).Error; err != nil {
return err
}
if err := UsersPreload(db).Find(&config.Users).Error; err != nil {
if err := dbmodels.UsersPreload(db).Find(&config.Users).Error; err != nil {
return err
}
if err := UserGroupsPreload(db).Find(&config.UserGroups).Error; err != nil {
if err := dbmodels.UserGroupsPreload(db).Find(&config.UserGroups).Error; err != nil {
return err
}
if err := HostGroupsPreload(db).Find(&config.HostGroups).Error; err != nil {
if err := dbmodels.HostGroupsPreload(db).Find(&config.HostGroups).Error; err != nil {
return err
}
if err := ACLsPreload(db).Find(&config.ACLs).Error; err != nil {
if err := dbmodels.ACLsPreload(db).Find(&config.ACLs).Error; err != nil {
return err
}
if err := db.Find(&config.Settings).Error; err != nil {
return err
}
if err := SessionsPreload(db).Find(&config.Sessions).Error; err != nil {
if err := dbmodels.SessionsPreload(db).Find(&config.Sessions).Error; err != nil {
return err
}
if !c.Bool("ignore-events") {
if err := EventsPreload(db).Find(&config.Events).Error; err != nil {
if err := dbmodels.EventsPreload(db).Find(&config.Events).Error; err != nil {
return err
}
}
@@ -409,7 +414,7 @@ GLOBAL OPTIONS:
return err
}
config := Config{}
config := dbmodels.Config{}
dec := json.NewDecoder(s)
if err := dec.Decode(&config); err != nil {
@@ -472,9 +477,9 @@ GLOBAL OPTIONS:
}
}
for _, host := range config.Hosts {
HostDecrypt(actx.config.aesKey, host)
crypto.HostDecrypt(actx.aesKey, host)
if !c.Bool("decrypt") {
if err := HostEncrypt(actx.config.aesKey, host); err != nil {
if err := crypto.HostEncrypt(actx.aesKey, host); err != nil {
return err
}
}
@@ -508,9 +513,9 @@ GLOBAL OPTIONS:
}
}
for _, sshKey := range config.SSHKeys {
SSHKeyDecrypt(actx.config.aesKey, sshKey)
crypto.SSHKeyDecrypt(actx.aesKey, sshKey)
if !c.Bool("decrypt") {
if err := SSHKeyEncrypt(actx.config.aesKey, sshKey); err != nil {
if err := crypto.SSHKeyEncrypt(actx.aesKey, sshKey); err != nil {
return err
}
}
@@ -570,8 +575,8 @@ GLOBAL OPTIONS:
return err
}
var events []*Event
if err := EventsPreload(EventsByIdentifiers(db, c.Args())).Find(&events).Error; err != nil {
var events []*dbmodels.Event
if err := dbmodels.EventsPreload(dbmodels.EventsByIdentifiers(db, c.Args())).Find(&events).Error; err != nil {
return err
}
@@ -599,10 +604,10 @@ GLOBAL OPTIONS:
return err
}
var events []Event
var events []dbmodels.Event
query := db.Order("created_at desc").Preload("Author")
if c.Bool("latest") {
var event Event
var event dbmodels.Event
if err := query.First(&event).Error; err != nil {
return err
}
@@ -670,20 +675,29 @@ GLOBAL OPTIONS:
return err
}
u, err := ParseInputURL(c.Args().First())
u, err := parseInputURL(c.Args().First())
if err != nil {
return err
}
host := &Host{
host := &dbmodels.Host{
URL: u.String(),
Comment: c.String("comment"),
}
if c.String("password") != "" {
host.Password = c.String("password")
}
host.Name = strings.Split(host.Hostname(), ".")[0]
matched, err := regexp.MatchString(`^([0-9]{1,3}.){3}.([0-9]{1,3})$`, host.Hostname())
if err != nil {
return err
}
if matched {
host.Name = host.Hostname()
} else {
host.Name = strings.Split(host.Hostname(), ".")[0]
}
if c.String("hop") != "" {
hop, err := HostByName(db, c.String("hop"))
hop, err := dbmodels.HostByName(db, c.String("hop"))
if err != nil {
return err
}
@@ -703,8 +717,8 @@ GLOBAL OPTIONS:
inputKey = "default"
}
if inputKey != "" {
var key SSHKey
if err := SSHKeysByIdentifiers(db, []string{inputKey}).First(&key).Error; err != nil {
var key dbmodels.SSHKey
if err := dbmodels.SSHKeysByIdentifiers(db, []string{inputKey}).First(&key).Error; err != nil {
return err
}
host.SSHKeyID = key.ID
@@ -715,12 +729,12 @@ GLOBAL OPTIONS:
if len(inputGroups) == 0 {
inputGroups = []string{"default"}
}
if err := HostGroupsByIdentifiers(db, inputGroups).Find(&host.Groups).Error; err != nil {
if err := dbmodels.HostGroupsByIdentifiers(db, inputGroups).Find(&host.Groups).Error; err != nil {
return err
}
// encrypt
if err := HostEncrypt(actx.config.aesKey, host); err != nil {
if err := crypto.HostEncrypt(actx.aesKey, host); err != nil {
return err
}
@@ -746,18 +760,18 @@ GLOBAL OPTIONS:
return err
}
var hosts []*Host
var hosts []*dbmodels.Host
db = db.Preload("Groups")
if myself.HasRole("admin") {
db = db.Preload("SSHKey")
}
if err := HostsByIdentifiers(db, c.Args()).Find(&hosts).Error; err != nil {
if err := dbmodels.HostsByIdentifiers(db, c.Args()).Find(&hosts).Error; err != nil {
return err
}
if c.Bool("decrypt") {
for _, host := range hosts {
HostDecrypt(actx.config.aesKey, host)
crypto.HostDecrypt(actx.aesKey, host)
}
}
@@ -777,10 +791,10 @@ GLOBAL OPTIONS:
return err
}
var hosts []*Host
var hosts []*dbmodels.Host
query := db.Order("created_at desc").Preload("Groups")
if c.Bool("latest") {
var host Host
var host dbmodels.Host
if err := query.First(&host).Error; err != nil {
return err
}
@@ -805,7 +819,7 @@ GLOBAL OPTIONS:
for _, host := range hosts {
authKey := ""
if host.SSHKeyID > 0 {
var key SSHKey
var key dbmodels.SSHKey
db.Model(&host).Related(&key)
authKey = key.Name
}
@@ -815,7 +829,7 @@ GLOBAL OPTIONS:
}
var hop string
if host.HopID != 0 {
var hopHost Host
var hopHost dbmodels.Host
db.Model(&host).Related(&hopHost, "HopID")
hop = hopHost.Name
} else {
@@ -850,7 +864,7 @@ GLOBAL OPTIONS:
return err
}
return HostsByIdentifiers(db, c.Args()).Delete(&Host{}).Error
return dbmodels.HostsByIdentifiers(db, c.Args()).Delete(&dbmodels.Host{}).Error
},
}, {
Name: "update",
@@ -875,8 +889,8 @@ GLOBAL OPTIONS:
return err
}
var hosts []Host
if err := HostsByIdentifiers(db, c.Args()).Find(&hosts).Error; err != nil {
var hosts []dbmodels.Host
if err := dbmodels.HostsByIdentifiers(db, c.Args()).Find(&hosts).Error; err != nil {
return err
}
@@ -899,7 +913,7 @@ GLOBAL OPTIONS:
// url
if c.String("url") != "" {
u, err := ParseInputURL(c.String("url"))
u, err := parseInputURL(c.String("url"))
if err != nil {
tx.Rollback()
return err
@@ -912,7 +926,7 @@ GLOBAL OPTIONS:
// hop
if c.String("hop") != "" {
hop, err := HostByName(db, c.String("hop"))
hop, err := dbmodels.HostByName(db, c.String("hop"))
if err != nil {
tx.Rollback()
return err
@@ -925,9 +939,10 @@ GLOBAL OPTIONS:
// remove the hop
if c.Bool("unset-hop") {
var hopHost Host
var hopHost dbmodels.Host
db.Model(&host).Related(&hopHost, "HopID")
if err := model.Association("Hop").Delete(hopHost).Error; err != nil {
if err := model.Association("Hop").Clear().Error; err != nil {
tx.Rollback()
return err
}
@@ -935,8 +950,8 @@ GLOBAL OPTIONS:
// associations
if c.String("key") != "" {
var key SSHKey
if err := SSHKeysByIdentifiers(db, []string{c.String("key")}).First(&key).Error; err != nil {
var key dbmodels.SSHKey
if err := dbmodels.SSHKeysByIdentifiers(db, []string{c.String("key")}).First(&key).Error; err != nil {
tx.Rollback()
return err
}
@@ -945,13 +960,13 @@ GLOBAL OPTIONS:
return err
}
}
var appendGroups []HostGroup
var deleteGroups []HostGroup
if err := HostGroupsByIdentifiers(db, c.StringSlice("assign-group")).Find(&appendGroups).Error; err != nil {
var appendGroups []dbmodels.HostGroup
var deleteGroups []dbmodels.HostGroup
if err := dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("assign-group")).Find(&appendGroups).Error; err != nil {
tx.Rollback()
return err
}
if err := HostGroupsByIdentifiers(db, c.StringSlice("unassign-group")).Find(&deleteGroups).Error; err != nil {
if err := dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("unassign-group")).Find(&deleteGroups).Error; err != nil {
tx.Rollback()
return err
}
@@ -988,7 +1003,7 @@ GLOBAL OPTIONS:
return err
}
hostGroup := HostGroup{
hostGroup := dbmodels.HostGroup{
Name: c.String("name"),
Comment: c.String("comment"),
}
@@ -1019,8 +1034,8 @@ GLOBAL OPTIONS:
return err
}
var hostGroups []HostGroup
if err := HostGroupsPreload(HostGroupsByIdentifiers(db, c.Args())).Find(&hostGroups).Error; err != nil {
var hostGroups []dbmodels.HostGroup
if err := dbmodels.HostGroupsPreload(dbmodels.HostGroupsByIdentifiers(db, c.Args())).Find(&hostGroups).Error; err != nil {
return err
}
@@ -1040,10 +1055,10 @@ GLOBAL OPTIONS:
return err
}
var hostGroups []*HostGroup
var hostGroups []*dbmodels.HostGroup
query := db.Order("created_at desc").Preload("ACLs").Preload("Hosts")
if c.Bool("latest") {
var hostGroup HostGroup
var hostGroup dbmodels.HostGroup
if err := query.First(&hostGroup).Error; err != nil {
return err
}
@@ -1093,7 +1108,7 @@ GLOBAL OPTIONS:
return err
}
return HostGroupsByIdentifiers(db, c.Args()).Delete(&HostGroup{}).Error
return dbmodels.HostGroupsByIdentifiers(db, c.Args()).Delete(&dbmodels.HostGroup{}).Error
},
}, {
Name: "update",
@@ -1112,8 +1127,8 @@ GLOBAL OPTIONS:
return err
}
var hostgroups []HostGroup
if err := HostGroupsByIdentifiers(db, c.Args()).Find(&hostgroups).Error; err != nil {
var hostgroups []dbmodels.HostGroup
if err := dbmodels.HostGroupsByIdentifiers(db, c.Args()).Find(&hostgroups).Error; err != nil {
return err
}
@@ -1146,14 +1161,14 @@ GLOBAL OPTIONS:
return err
}
fmt.Fprintf(s, "debug mode (server): %v\n", actx.config.debug)
fmt.Fprintf(s, "debug mode (server): %v\n", actx.debug)
hostname, _ := os.Hostname()
fmt.Fprintf(s, "Hostname: %s\n", hostname)
fmt.Fprintf(s, "CPUs: %d\n", runtime.NumCPU())
fmt.Fprintf(s, "Demo mode: %v\n", actx.config.demo)
fmt.Fprintf(s, "DB Driver: %s\n", actx.config.dbDriver)
fmt.Fprintf(s, "DB Conn: %s\n", actx.config.dbURL)
fmt.Fprintf(s, "Bind Address: %s\n", actx.config.bindAddr)
fmt.Fprintf(s, "Demo mode: %v\n", actx.demo)
fmt.Fprintf(s, "DB Driver: %s\n", actx.dbDriver)
fmt.Fprintf(s, "DB Conn: %s\n", actx.dbURL)
fmt.Fprintf(s, "Bind Address: %s\n", actx.bindAddr)
fmt.Fprintf(s, "System Time: %v\n", time.Now().Format(time.RFC3339Nano))
fmt.Fprintf(s, "OS Type: %s\n", runtime.GOOS)
fmt.Fprintf(s, "OS Architecture: %s\n", runtime.GOARCH)
@@ -1163,10 +1178,10 @@ GLOBAL OPTIONS:
fmt.Fprintf(s, "User ID: %v\n", myself.ID)
fmt.Fprintf(s, "User email: %s\n", myself.Email)
fmt.Fprintf(s, "Version: %s\n", Version)
fmt.Fprintf(s, "GIT SHA: %s\n", GitSha)
fmt.Fprintf(s, "GIT Branch: %s\n", GitBranch)
fmt.Fprintf(s, "GIT Tag: %s\n", GitTag)
fmt.Fprintf(s, "Version: %s\n", version)
fmt.Fprintf(s, "GIT SHA: %s\n", gitSha)
fmt.Fprintf(s, "GIT Branch: %s\n", gitBranch)
fmt.Fprintf(s, "GIT Tag: %s\n", gitTag)
// FIXME: add info about current server (network, cpu, ram, OS)
// FIXME: add info about current user
@@ -1198,9 +1213,9 @@ GLOBAL OPTIONS:
name = c.String("name")
}
key, err := NewSSHKey(c.String("type"), c.Uint("length"))
if actx.config.aesKey != "" {
if err2 := SSHKeyEncrypt(actx.config.aesKey, key); err2 != nil {
key, err := crypto.NewSSHKey(c.String("type"), c.Uint("length"))
if actx.aesKey != "" {
if err2 := crypto.SSHKeyEncrypt(actx.aesKey, key); err2 != nil {
return err2
}
}
@@ -1255,7 +1270,7 @@ GLOBAL OPTIONS:
break
}
}
key, err := ImportSSHKey(value)
key, err := crypto.ImportSSHKey(value)
if err != nil {
return err
}
@@ -1292,14 +1307,14 @@ GLOBAL OPTIONS:
return err
}
var keys []*SSHKey
if err := SSHKeysByIdentifiers(SSHKeysPreload(db), c.Args()).Find(&keys).Error; err != nil {
var keys []*dbmodels.SSHKey
if err := dbmodels.SSHKeysByIdentifiers(dbmodels.SSHKeysPreload(db), c.Args()).Find(&keys).Error; err != nil {
return err
}
if c.Bool("decrypt") {
for _, key := range keys {
SSHKeyDecrypt(actx.config.aesKey, key)
crypto.SSHKeyDecrypt(actx.aesKey, key)
}
}
@@ -1319,10 +1334,10 @@ GLOBAL OPTIONS:
return err
}
var sshKeys []*SSHKey
var sshKeys []*dbmodels.SSHKey
query := db.Order("created_at desc").Preload("Hosts")
if c.Bool("latest") {
var sshKey SSHKey
var sshKey dbmodels.SSHKey
if err := query.First(&sshKey).Error; err != nil {
return err
}
@@ -1373,7 +1388,7 @@ GLOBAL OPTIONS:
return err
}
return SSHKeysByIdentifiers(db, c.Args()).Delete(&SSHKey{}).Error
return dbmodels.SSHKeysByIdentifiers(db, c.Args()).Delete(&dbmodels.SSHKey{}).Error
},
}, {
Name: "setup",
@@ -1386,8 +1401,8 @@ GLOBAL OPTIONS:
// not checking roles, everyone with an account can see how to enroll new hosts
var key SSHKey
if err := SSHKeysByIdentifiers(db, c.Args()).First(&key).Error; err != nil {
var key dbmodels.SSHKey
if err := dbmodels.SSHKeysByIdentifiers(db, c.Args()).First(&key).Error; err != nil {
return err
}
fmt.Fprintf(s, "umask 077; mkdir -p .ssh; echo %s sshportal >> .ssh/authorized_keys\n", key.PubKey)
@@ -1404,11 +1419,11 @@ GLOBAL OPTIONS:
// not checking roles, everyone with an account can see how to enroll new hosts
var key SSHKey
if err := SSHKeysByIdentifiers(SSHKeysPreload(db), c.Args()).First(&key).Error; err != nil {
var key dbmodels.SSHKey
if err := dbmodels.SSHKeysByIdentifiers(dbmodels.SSHKeysPreload(db), c.Args()).First(&key).Error; err != nil {
return err
}
SSHKeyDecrypt(actx.config.aesKey, &key)
crypto.SSHKeyDecrypt(actx.aesKey, &key)
type line struct {
key string
@@ -1486,8 +1501,8 @@ GLOBAL OPTIONS:
return err
}
var users []User
if err := UsersPreload(UsersByIdentifiers(db, c.Args())).Find(&users).Error; err != nil {
var users []dbmodels.User
if err := dbmodels.UsersPreload(dbmodels.UsersByIdentifiers(db, c.Args())).Find(&users).Error; err != nil {
return err
}
@@ -1522,7 +1537,7 @@ GLOBAL OPTIONS:
name = c.String("name")
}
user := User{
user := dbmodels.User{
Name: name,
Email: email,
Comment: c.String("comment"),
@@ -1538,7 +1553,7 @@ GLOBAL OPTIONS:
if len(inputGroups) == 0 {
inputGroups = []string{"default"}
}
if err := UserGroupsByIdentifiers(db, inputGroups).Find(&user.Groups).Error; err != nil {
if err := dbmodels.UserGroupsByIdentifiers(db, inputGroups).Find(&user.Groups).Error; err != nil {
return err
}
@@ -1561,10 +1576,10 @@ GLOBAL OPTIONS:
return err
}
var users []*User
var users []*dbmodels.User
query := db.Order("created_at desc").Preload("Groups").Preload("Roles").Preload("Keys")
if c.Bool("latest") {
var user User
var user dbmodels.User
if err := query.First(&user).Error; err != nil {
return err
}
@@ -1622,7 +1637,7 @@ GLOBAL OPTIONS:
return err
}
return UsersByIdentifiers(db, c.Args()).Delete(&User{}).Error
return dbmodels.UsersByIdentifiers(db, c.Args()).Delete(&dbmodels.User{}).Error
},
}, {
Name: "update",
@@ -1646,8 +1661,8 @@ GLOBAL OPTIONS:
}
// FIXME: check if unset-admin + user == myself
var users []User
if err := UsersByIdentifiers(db, c.Args()).Find(&users).Error; err != nil {
var users []dbmodels.User
if err := dbmodels.UsersByIdentifiers(db, c.Args()).Find(&users).Error; err != nil {
return err
}
@@ -1673,13 +1688,13 @@ GLOBAL OPTIONS:
}
// associations
var appendGroups []UserGroup
if err := UserGroupsByIdentifiers(db, c.StringSlice("assign-group")).Find(&appendGroups).Error; err != nil {
var appendGroups []dbmodels.UserGroup
if err := dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("assign-group")).Find(&appendGroups).Error; err != nil {
tx.Rollback()
return err
}
var deleteGroups []UserGroup
if err := UserGroupsByIdentifiers(db, c.StringSlice("unassign-group")).Find(&deleteGroups).Error; err != nil {
var deleteGroups []dbmodels.UserGroup
if err := dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("unassign-group")).Find(&deleteGroups).Error; err != nil {
tx.Rollback()
return err
}
@@ -1693,13 +1708,13 @@ GLOBAL OPTIONS:
return err
}
}
var appendRoles []UserRole
if err := UserRolesByIdentifiers(db, c.StringSlice("assign-role")).Find(&appendRoles).Error; err != nil {
var appendRoles []dbmodels.UserRole
if err := dbmodels.UserRolesByIdentifiers(db, c.StringSlice("assign-role")).Find(&appendRoles).Error; err != nil {
tx.Rollback()
return err
}
var deleteRoles []UserRole
if err := UserRolesByIdentifiers(db, c.StringSlice("unassign-role")).Find(&deleteRoles).Error; err != nil {
var deleteRoles []dbmodels.UserRole
if err := dbmodels.UserRolesByIdentifiers(db, c.StringSlice("unassign-role")).Find(&deleteRoles).Error; err != nil {
tx.Rollback()
return err
}
@@ -1735,7 +1750,7 @@ GLOBAL OPTIONS:
return err
}
userGroup := UserGroup{
userGroup := dbmodels.UserGroup{
Name: c.String("name"),
Comment: c.String("comment"),
}
@@ -1749,7 +1764,7 @@ GLOBAL OPTIONS:
// FIXME: check if name already exists
// FIXME: add myself to the new group
userGroup.Users = []*User{myself}
userGroup.Users = []*dbmodels.User{myself}
if err := db.Create(&userGroup).Error; err != nil {
return err
@@ -1770,8 +1785,8 @@ GLOBAL OPTIONS:
return err
}
var userGroups []UserGroup
if err := UserGroupsPreload(UserGroupsByIdentifiers(db, c.Args())).Find(&userGroups).Error; err != nil {
var userGroups []dbmodels.UserGroup
if err := dbmodels.UserGroupsPreload(dbmodels.UserGroupsByIdentifiers(db, c.Args())).Find(&userGroups).Error; err != nil {
return err
}
@@ -1791,10 +1806,10 @@ GLOBAL OPTIONS:
return err
}
var userGroups []*UserGroup
var userGroups []*dbmodels.UserGroup
query := db.Order("created_at desc").Preload("ACLs").Preload("Users")
if c.Bool("latest") {
var userGroup UserGroup
var userGroup dbmodels.UserGroup
if err := query.First(&userGroup).Error; err != nil {
return err
}
@@ -1843,7 +1858,7 @@ GLOBAL OPTIONS:
return err
}
return UserGroupsByIdentifiers(db, c.Args()).Delete(&UserGroup{}).Error
return dbmodels.UserGroupsByIdentifiers(db, c.Args()).Delete(&dbmodels.UserGroup{}).Error
},
}, {
Name: "update",
@@ -1862,8 +1877,8 @@ GLOBAL OPTIONS:
return err
}
var usergroups []UserGroup
if err := UserGroupsByIdentifiers(db, c.Args()).Find(&usergroups).Error; err != nil {
var usergroups []dbmodels.UserGroup
if err := dbmodels.UserGroupsByIdentifiers(db, c.Args()).Find(&usergroups).Error; err != nil {
return err
}
@@ -1909,8 +1924,8 @@ GLOBAL OPTIONS:
return err
}
var user User
if err := UsersByIdentifiers(db, c.Args()).First(&user).Error; err != nil {
var user dbmodels.User
if err := dbmodels.UsersByIdentifiers(db, c.Args()).First(&user).Error; err != nil {
return err
}
@@ -1923,10 +1938,11 @@ GLOBAL OPTIONS:
return err
}
userkey := UserKey{
User: &user,
Key: key.Marshal(),
Comment: comment,
userkey := dbmodels.UserKey{
User: &user,
Key: key.Marshal(),
Comment: comment,
AuthorizedKey: string(gossh.MarshalAuthorizedKey(key)),
}
if c.String("comment") != "" {
userkey.Comment = c.String("comment")
@@ -1956,8 +1972,8 @@ GLOBAL OPTIONS:
return err
}
var userKeys []UserKey
if err := UserKeysPreload(UserKeysByIdentifiers(db, c.Args())).Find(&userKeys).Error; err != nil {
var userKeys []dbmodels.UserKey
if err := dbmodels.UserKeysPreload(dbmodels.UserKeysByIdentifiers(db, c.Args())).Find(&userKeys).Error; err != nil {
return err
}
@@ -1977,10 +1993,10 @@ GLOBAL OPTIONS:
return err
}
var userKeys []*UserKey
var userKeys []*dbmodels.UserKey
query := db.Order("created_at desc").Preload("User")
if c.Bool("latest") {
var userKey UserKey
var userKey dbmodels.UserKey
if err := query.First(&userKey).Error; err != nil {
return err
}
@@ -2031,7 +2047,7 @@ GLOBAL OPTIONS:
return err
}
return UserKeysByIdentifiers(db, c.Args()).Delete(&UserKey{}).Error
return dbmodels.UserKeysByIdentifiers(db, c.Args()).Delete(&dbmodels.UserKey{}).Error
},
},
},
@@ -2052,8 +2068,8 @@ GLOBAL OPTIONS:
return err
}
var sessions []Session
if err := SessionsPreload(SessionsByIdentifiers(db, c.Args())).Find(&sessions).Error; err != nil {
var sessions []dbmodels.Session
if err := dbmodels.SessionsPreload(dbmodels.SessionsByIdentifiers(db, c.Args())).Find(&sessions).Error; err != nil {
return err
}
@@ -2066,6 +2082,7 @@ GLOBAL OPTIONS:
Usage: "Lists sessions",
Flags: []cli.Flag{
cli.BoolFlag{Name: "latest, l", Usage: "Show the latest session"},
cli.BoolFlag{Name: "active, a", Usage: "Show only active session"},
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
},
Action: func(c *cli.Context) error {
@@ -2073,10 +2090,17 @@ GLOBAL OPTIONS:
return err
}
var sessions []*Session
query := db.Order("created_at desc").Preload("User").Preload("Host")
var sessions []*dbmodels.Session
limit, offset, status := 60000, -1, []string{string(dbmodels.SessionStatusActive), string(dbmodels.SessionStatusClosed), string(dbmodels.SessionStatusUnknown)}
if c.Bool("active") {
status = status[:1]
}
query := db.Order("created_at desc").Limit(limit).Offset(offset).Where("status in (?)", status).Preload("User").Preload("Host")
if c.Bool("latest") {
var session Session
var session dbmodels.Session
if err := query.First(&session).Error; err != nil {
return err
}
@@ -2085,6 +2109,20 @@ GLOBAL OPTIONS:
if err := query.Find(&sessions).Error; err != nil {
return err
}
factor := 1
for len(sessions) >= limit*factor {
var additionnalSessions []*dbmodels.Session
offset = limit * factor
query := db.Order("created_at desc").Limit(limit).Offset(offset).Where("status in (?)", status).Preload("User").Preload("Host")
if err := query.Find(&additionnalSessions).Error; err != nil {
return err
}
sessions = append(sessions, additionnalSessions...)
factor++
}
}
if c.Bool("quiet") {
for _, session := range sessions {
@@ -2133,7 +2171,7 @@ GLOBAL OPTIONS:
Name: "version",
Usage: "Shows the SSHPortal version information",
Action: func(c *cli.Context) error {
fmt.Fprintf(s, "%s\n", Version)
fmt.Fprintf(s, "%s\n", version)
return nil
},
}, {
@@ -2164,7 +2202,7 @@ GLOBAL OPTIONS:
if len(words) == 0 {
continue
}
NewEvent("shell", words[0]).SetAuthor(myself).SetArg("interactive", true).SetArg("args", words[1:]).Log(db)
dbmodels.NewEvent("shell", words[0]).SetAuthor(myself).SetArg("interactive", true).SetArg("args", words[1:]).Log(db)
if err := app.Run(append([]string{"config"}, words...)); err != nil {
if cliErr, ok := err.(*cli.ExitError); ok {
if cliErr.ExitCode() != 0 {
@@ -2177,7 +2215,7 @@ GLOBAL OPTIONS:
}
}
} else { // oneshot mode
NewEvent("shell", sshCommand[0]).SetAuthor(myself).SetArg("interactive", false).SetArg("args", sshCommand[1:]).Log(db)
dbmodels.NewEvent("shell", sshCommand[0]).SetAuthor(myself).SetArg("interactive", false).SetArg("args", sshCommand[1:]).Log(db)
if err := app.Run(append([]string{"config"}, sshCommand...)); err != nil {
if errMsg := err.Error(); errMsg != "" {
fmt.Fprintf(s, "error: %s\n", errMsg)
@@ -2191,3 +2229,21 @@ GLOBAL OPTIONS:
return nil
}
func wrapText(in string, length int) string {
if len(in) <= length {
return in
}
return in[0:length-3] + "..."
}
func parseInputURL(input string) (*url.URL, error) {
if !strings.Contains(input, "://") {
input = "ssh://" + input
}
u, err := url.Parse(input)
if err != nil {
return nil, err
}
return u, nil
}

View File

@@ -1,4 +1,4 @@
package main
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"bytes"
@@ -11,8 +11,9 @@ import (
"github.com/gliderlabs/ssh"
"github.com/jinzhu/gorm"
"github.com/moul/sshportal/pkg/bastionsession"
gossh "golang.org/x/crypto/ssh"
"moul.io/sshportal/pkg/crypto"
"moul.io/sshportal/pkg/dbmodels"
)
type sshportalContextKey string
@@ -20,56 +21,44 @@ type sshportalContextKey string
var authContextKey = sshportalContextKey("auth")
type authContext struct {
message string
err error
user User
inputUsername string
db *gorm.DB
userKey UserKey
config *configServe
authMethod string
authSuccess bool
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
type userType string
const (
UserTypeHealthcheck UserType = "healthcheck"
UserTypeBastion = "bastion"
UserTypeInvite = "invite"
UserTypeShell = "shell"
userTypeHealthcheck userType = "healthcheck"
userTypeBastion userType = "bastion"
userTypeInvite userType = "invite"
userTypeShell userType = "shell"
)
type SessionType string
const (
SessionTypeBastion SessionType = "bastion"
SessionTypeShell = "shell"
)
func (c authContext) userType() UserType {
func (c authContext) userType() userType {
switch {
case c.inputUsername == "healthcheck":
return UserTypeHealthcheck
return userTypeHealthcheck
case c.inputUsername == c.user.Name || c.inputUsername == c.user.Email || c.inputUsername == "admin":
return UserTypeShell
return userTypeShell
case strings.HasPrefix(c.inputUsername, "invite:"):
return UserTypeInvite
return userTypeInvite
default:
return UserTypeBastion
return userTypeBastion
}
}
func (c authContext) sessionType() SessionType {
switch c.userType() {
case "bastion":
return SessionTypeBastion
default:
return SessionTypeShell
}
}
func dynamicHostKey(db *gorm.DB, host *Host) gossh.HostKeyCallback {
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...")
@@ -83,7 +72,9 @@ func dynamicHostKey(db *gorm.DB, host *Host) gossh.HostKeyCallback {
}
}
func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
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":
@@ -98,9 +89,9 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
actx := ctx.Value(authContextKey).(*authContext)
switch actx.userType() {
case UserTypeBastion:
log.Printf("New connection(bastion): sshUser=%q remote=%q local=%q dbUser=id:%q,email:%s", conn.User(), conn.RemoteAddr(), conn.LocalAddr(), actx.user.ID, actx.user.Email)
host, err := HostByName(actx.db, actx.inputUsername)
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 {
@@ -113,8 +104,8 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
}
switch host.Scheme() {
case BastionSchemeSSH:
sessionConfigs := make([]bastionsession.Config, 0)
case dbmodels.BastionSchemeSSH:
sessionConfigs := make([]sessionConfig, 0)
currentHost := host
for currentHost != nil {
clientConfig, err2 := bastionClientConfig(ctx, currentHost)
@@ -128,25 +119,25 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
_ = ch.Close()
return
}
sessionConfigs = append([]bastionsession.Config{{
sessionConfigs = append([]sessionConfig{{
Addr: currentHost.DialAddr(),
ClientConfig: clientConfig,
Logs: actx.config.logsLocation,
Logs: actx.logsLocation,
}}, sessionConfigs...)
if currentHost.HopID != 0 {
var newHost Host
var newHost dbmodels.Host
actx.db.Model(currentHost).Related(&newHost, "HopID")
hostname := newHost.Name
currentHost, _ = HostByName(actx.db, hostname)
currentHost, _ = dbmodels.HostByName(actx.db, hostname)
} else {
currentHost = nil
}
}
sess := Session{
sess := dbmodels.Session{
UserID: actx.user.ID,
HostID: host.ID,
Status: SessionStatusActive,
Status: string(dbmodels.SessionStatusActive),
}
if err = actx.db.Create(&sess).Error; err != nil {
ch, _, err2 := newChan.Accept()
@@ -157,31 +148,30 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
_ = ch.Close()
return
}
go func() {
err = bastionsession.MultiChannelHandler(srv, conn, newChan, ctx, sessionConfigs)
err = multiChannelHandler(srv, conn, newChan, ctx, sessionConfigs, sess.ID)
if err != nil {
log.Printf("Error: %v", err)
}
}()
now := time.Now()
sessUpdate := Session{
Status: SessionStatusClosed,
ErrMsg: fmt.Sprintf("%v", err),
StoppedAt: &now,
}
switch sessUpdate.ErrMsg {
case "lch closed the connection", "rch closed the connection":
sessUpdate.ErrMsg = ""
}
actx.db.Model(&sess).Updates(&sessUpdate)
case BastionSchemeTelnet:
now := time.Now()
sessUpdate := dbmodels.Session{
Status: string(dbmodels.SessionStatusClosed),
ErrMsg: fmt.Sprintf("%v", err),
StoppedAt: &now,
}
switch sessUpdate.ErrMsg {
case "lch closed the connection", "rch closed the connection":
sessUpdate.ErrMsg = ""
}
actx.db.Model(&sess).Updates(&sessUpdate)
}()
case dbmodels.BastionSchemeTelnet:
tmpSrv := ssh.Server{
// PtyCallback: srv.PtyCallback,
Handler: telnetHandler(host),
}
ssh.DefaultChannelHandler(&tmpSrv, conn, newChan, ctx)
DefaultChannelHandler(&tmpSrv, conn, newChan, ctx)
default:
ch, _, err2 := newChan.Accept()
if err2 != nil {
@@ -192,37 +182,37 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
_ = ch.Close()
}
default: // shell
ssh.DefaultChannelHandler(srv, conn, newChan, ctx)
DefaultChannelHandler(srv, conn, newChan, ctx)
}
}
func bastionClientConfig(ctx ssh.Context, host *Host) (*gossh.ClientConfig, error) {
func bastionClientConfig(ctx ssh.Context, host *dbmodels.Host) (*gossh.ClientConfig, error) {
actx := ctx.Value(authContextKey).(*authContext)
clientConfig, err := host.clientConfig(dynamicHostKey(actx.db, host))
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 User
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 Host
var tmpHost dbmodels.Host
if err = actx.db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", host.ID).First(&tmpHost).Error; err != nil {
return nil, err
}
action, err2 := CheckACLs(tmpUser, tmpHost)
action, err2 := checkACLs(tmpUser, tmpHost)
if err2 != nil {
return nil, err2
}
HostDecrypt(actx.config.aesKey, host)
SSHKeyDecrypt(actx.config.aesKey, host.SSHKey)
switch action {
case ACLActionAllow:
case ACLActionDeny:
case string(dbmodels.ACLActionAllow):
case string(dbmodels.ACLActionDeny):
return nil, fmt.Errorf("you don't have permission to that host")
default:
return nil, fmt.Errorf("invalid ACL action: %q", action)
@@ -230,10 +220,10 @@ func bastionClientConfig(ctx ssh.Context, host *Host) (*gossh.ClientConfig, erro
return clientConfig, nil
}
func shellHandler(s ssh.Session) {
func ShellHandler(s ssh.Session, version, gitSha, gitTag, gitBranch string) {
actx := s.Context().Value(authContextKey).(*authContext)
if actx.userType() != UserTypeHealthcheck {
log.Printf("New connection(shell): sshUser=%q remote=%q local=%q command=%q dbUser=id:%q,email:%s", s.User(), s.RemoteAddr(), s.LocalAddr(), s.Command(), actx.user.ID, actx.user.Email)
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 {
@@ -247,43 +237,48 @@ func shellHandler(s ssh.Session) {
}
switch actx.userType() {
case UserTypeHealthcheck:
case userTypeHealthcheck:
fmt.Fprintln(s, "OK")
return
case UserTypeShell:
if err := shell(s); err != nil {
case userTypeShell:
if err := shell(s, version, gitSha, gitTag, gitBranch); err != nil {
fmt.Fprintf(s, "error: %v\n", err)
_ = s.Exit(1)
}
return
case UserTypeInvite:
case userTypeInvite:
// do nothing (message was printed at the beginning of the function)
return
}
panic("should not happen")
}
func passwordAuthHandler(db *gorm.DB, cfg *configServe) ssh.PasswordHandler {
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(),
config: cfg,
logsLocation: logsLocation,
aesKey: aesKey,
dbDriver: dbDriver,
dbURL: dbURL,
bindAddr: bindAddr,
demo: demo,
authMethod: "password",
}
actx.authSuccess = actx.userType() == UserTypeHealthcheck
actx.authSuccess = actx.userType() == userTypeHealthcheck
ctx.SetValue(authContextKey, actx)
return actx.authSuccess
}
}
func privateKeyFromDB(db *gorm.DB, aesKey string) func(*ssh.Server) error {
func PrivateKeyFromDB(db *gorm.DB, aesKey string) func(*ssh.Server) error {
return func(srv *ssh.Server) error {
var key SSHKey
if err := SSHKeysByIdentifiers(db, []string{"host"}).First(&key).Error; err != nil {
var key dbmodels.SSHKey
if err := dbmodels.SSHKeysByIdentifiers(db, []string{"host"}).First(&key).Error; err != nil {
return err
}
SSHKeyDecrypt(aesKey, &key)
crypto.SSHKeyDecrypt(aesKey, &key)
signer, err := gossh.ParsePrivateKey([]byte(key.PrivKey))
if err != nil {
@@ -294,12 +289,17 @@ func privateKeyFromDB(db *gorm.DB, aesKey string) func(*ssh.Server) error {
}
}
func publicKeyAuthHandler(db *gorm.DB, cfg *configServe) ssh.PublicKeyHandler {
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(),
config: cfg,
logsLocation: logsLocation,
aesKey: aesKey,
dbDriver: dbDriver,
dbURL: dbURL,
bindAddr: bindAddr,
demo: demo,
authMethod: "pubkey",
authSuccess: true,
}
@@ -309,20 +309,20 @@ func publicKeyAuthHandler(db *gorm.DB, cfg *configServe) ssh.PublicKeyHandler {
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 {
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 {
if actx.userType() == userTypeInvite {
inputToken := strings.Split(actx.inputUsername, ":")[1]
if len(inputToken) > 0 {
db.Where("invite_token = ?", inputToken).First(&actx.user)
}
if actx.user.ID > 0 {
actx.userKey = UserKey{
actx.userKey = dbmodels.UserKey{
UserID: actx.user.ID,
Key: key.Marshal(),
Comment: "created by sshportal",
@@ -336,7 +336,7 @@ func publicKeyAuthHandler(db *gorm.DB, cfg *configServe) ssh.PublicKeyHandler {
actx.message = fmt.Sprintf("Welcome %s!\n\nYour key is now associated with the user %q.\n", actx.user.Name, actx.user.Email)
} else {
actx.user = User{Name: "Anonymous"}
actx.user = dbmodels.User{Name: "Anonymous"}
actx.err = errors.New("your token is invalid or expired")
}
return true
@@ -344,7 +344,7 @@ func publicKeyAuthHandler(db *gorm.DB, cfg *configServe) ssh.PublicKeyHandler {
// fallback
actx.err = errors.New("unknown ssh key")
actx.user = User{Name: "Anonymous"}
actx.user = dbmodels.User{Name: "Anonymous"}
return true
}
}

View File

@@ -1,4 +1,4 @@
package main
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"bufio"
@@ -11,6 +11,7 @@ import (
"github.com/gliderlabs/ssh"
oi "github.com/reiver/go-oi"
telnet "github.com/reiver/go-telnet"
"moul.io/sshportal/pkg/dbmodels"
)
type bastionTelnetCaller struct {
@@ -75,7 +76,7 @@ func scannerSplitFunc(data []byte, atEOF bool) (advance int, token []byte, err e
return bufio.ScanLines(data, atEOF)
}
func telnetHandler(host *Host) ssh.Handler {
func telnetHandler(host *dbmodels.Host) ssh.Handler {
return func(s ssh.Session) {
// FIXME: log session in db
//actx := s.Context().Value(authContextKey).(*authContext)

View File

@@ -1,4 +1,4 @@
package main
package crypto // import "moul.io/sshportal/pkg/crypto"
import (
"bytes"
@@ -15,10 +15,11 @@ import (
"strings"
gossh "golang.org/x/crypto/ssh"
"moul.io/sshportal/pkg/dbmodels"
)
func NewSSHKey(keyType string, length uint) (*SSHKey, error) {
key := SSHKey{
func NewSSHKey(keyType string, length uint) (*dbmodels.SSHKey, error) {
key := dbmodels.SSHKey{
Type: keyType,
Length: length,
}
@@ -53,8 +54,8 @@ func NewSSHKey(keyType string, length uint) (*SSHKey, error) {
return &key, nil
}
func ImportSSHKey(keyValue string) (*SSHKey, error) {
key := SSHKey{
func ImportSSHKey(keyValue string) (*dbmodels.SSHKey, error) {
key := dbmodels.SSHKey{
Type: "rsa",
}
@@ -132,7 +133,7 @@ func safeDecrypt(key []byte, cryptoText string) string {
return out
}
func HostEncrypt(aesKey string, host *Host) (err error) {
func HostEncrypt(aesKey string, host *dbmodels.Host) (err error) {
if aesKey == "" {
return nil
}
@@ -141,7 +142,7 @@ func HostEncrypt(aesKey string, host *Host) (err error) {
}
return
}
func HostDecrypt(aesKey string, host *Host) {
func HostDecrypt(aesKey string, host *dbmodels.Host) {
if aesKey == "" {
return
}
@@ -150,14 +151,14 @@ func HostDecrypt(aesKey string, host *Host) {
}
}
func SSHKeyEncrypt(aesKey string, key *SSHKey) (err error) {
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 *SSHKey) {
func SSHKeyDecrypt(aesKey string, key *dbmodels.SSHKey) {
if aesKey == "" {
return
}

View File

@@ -1,4 +1,4 @@
package main
package dbmodels // import "moul.io/sshportal/pkg/dbmodels"
import (
"encoding/json"
@@ -53,7 +53,7 @@ type SSHKey struct {
type Host struct {
// FIXME: use uuid for ID
gorm.Model
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
Name string `gorm:"size:32" valid:"required,length(1|32)"`
Addr string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
User string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
Password string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
@@ -70,7 +70,7 @@ type Host struct {
// UserKey defines a user public key used by sshportal to identify the user
type UserKey struct {
gorm.Model
Key []byte `sql:"size:10000" valid:"required,length(1|10000)"`
Key []byte `sql:"size:10000" valid:"length(1|10000)"`
AuthorizedKey string `sql:"size:10000" valid:"required,length(1|10000)"`
UserID uint ``
User *User `gorm:"ForeignKey:UserID"`
@@ -148,23 +148,22 @@ type SessionStatus string
const (
SessionStatusUnknown SessionStatus = "unknown"
SessionStatusActive = "active"
SessionStatusClosed = "closed"
SessionStatusActive SessionStatus = "active"
SessionStatusClosed SessionStatus = "closed"
)
type ACLAction string
const (
ACLActionUnknown ACLAction = "unknown"
ACLActionAllow = "allow"
ACLActionDeny = "deny"
ACLActionAllow ACLAction = "allow"
ACLActionDeny ACLAction = "deny"
)
type BastionScheme string
const (
BastionSchemeSSH BastionScheme = "ssh"
BastionSchemeTelnet = "telnet"
BastionSchemeTelnet BastionScheme = "telnet"
)
func init() {
@@ -181,16 +180,6 @@ func init() {
// Host helpers
func ParseInputURL(input string) (*url.URL, error) {
if !strings.Contains(input, "://") {
input = "ssh://" + input
}
u, err := url.Parse(input)
if err != nil {
return nil, err
}
return u, nil
}
func (host *Host) DialAddr() string {
return fmt.Sprintf("%s:%d", host.Hostname(), host.Port())
}
@@ -297,12 +286,12 @@ func HostByName(db *gorm.DB, name string) (*Host, error) {
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 nil, fmt.Errorf("no such target: %q", name)
}
return &host, nil
}
func (host *Host) clientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, error) {
func (host *Host) ClientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, error) {
config := gossh.ClientConfig{
User: host.Username(),
HostKeyCallback: hk,
@@ -396,9 +385,9 @@ func UserKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
// UserRole helpers
func UserRolesPreload(db *gorm.DB) *gorm.DB {
return db.Preload("Users")
}
//func UserRolesPreload(db *gorm.DB) *gorm.DB {
// return db.Preload("Users")
//}
func UserRolesByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
}
@@ -447,7 +436,7 @@ func (e *Event) Log(db *gorm.DB) {
}
func (e *Event) SetAuthor(user *User) *Event {
e.Author = user
//e.Author = user
e.AuthorID = user.ID
return e
}

View File

@@ -1,59 +0,0 @@
package logtunnel
import (
"encoding/binary"
"io"
"syscall"
"time"
"golang.org/x/crypto/ssh"
)
type logTunnel struct {
host string
channel ssh.Channel
writer io.WriteCloser
}
type ForwardData struct {
DestinationHost string
DestinationPort uint32
SourceHost string
SourcePort uint32
}
func writeHeader(fd io.Writer, length int) {
t := time.Now()
tv := syscall.NsecToTimeval(t.UnixNano())
binary.Write(fd, binary.LittleEndian, int32(tv.Sec))
binary.Write(fd, binary.LittleEndian, int32(tv.Usec))
binary.Write(fd, binary.LittleEndian, int32(length))
}
func New(channel ssh.Channel, writer io.WriteCloser, host string) *logTunnel {
return &logTunnel{
host: host,
channel: channel,
writer: writer,
}
}
func (l *logTunnel) Read(data []byte) (int, error) {
return l.Read(data)
}
func (l *logTunnel) Write(data []byte) (int, error) {
writeHeader(l.writer, len(data) + len(l.host + ": "))
l.writer.Write([]byte(l.host + ": "))
l.writer.Write(data)
return l.channel.Write(data)
}
func (l *logTunnel) Close() error {
l.writer.Close()
return l.channel.Close()
}

6
renovate.json Normal file
View File

@@ -0,0 +1,6 @@
{
"extends": [
"config:base"
],
"groupName": "all"
}

134
server.go Normal file
View File

@@ -0,0 +1,134 @@
package main
import (
"fmt"
"log"
"math"
"net"
"os"
"time"
"github.com/gliderlabs/ssh"
"github.com/jinzhu/gorm"
"github.com/urfave/cli"
gossh "golang.org/x/crypto/ssh"
"moul.io/sshportal/pkg/bastion"
)
type serverConfig struct {
aesKey string
dbDriver, dbURL string
logsLocation string
bindAddr string
debug, demo bool
idleTimeout time.Duration
}
func parseServerConfig(c *cli.Context) (*serverConfig, error) {
ret := &serverConfig{
aesKey: c.String("aes-key"),
dbDriver: c.String("db-driver"),
dbURL: c.String("db-conn"),
bindAddr: c.String("bind-address"),
debug: c.Bool("debug"),
demo: c.Bool("demo"),
logsLocation: c.String("logs-location"),
idleTimeout: c.Duration("idle-timeout"),
}
switch len(ret.aesKey) {
case 0, 16, 24, 32:
default:
return nil, fmt.Errorf("invalid aes key size, should be 16 or 24, 32")
}
return ret, nil
}
func ensureLogDirectory(location string) error {
// check for the logdir existence
logsLocation, err := os.Stat(location)
if err != nil {
if os.IsNotExist(err) {
return os.MkdirAll(location, os.ModeDir|os.FileMode(0750))
}
return err
}
if !logsLocation.IsDir() {
return fmt.Errorf("log directory cannot be created")
}
return nil
}
func server(c *serverConfig) (err error) {
var db = (*gorm.DB)(nil)
// try to setup the local DB
if db, err = gorm.Open(c.dbDriver, c.dbURL); err != nil {
return
}
defer func() {
origErr := err
err = db.Close()
if origErr != nil {
err = origErr
}
}()
if err = db.DB().Ping(); err != nil {
return
}
db.LogMode(c.debug)
if err = bastion.DBInit(db); err != nil {
return
}
// create TCP listening socket
ln, err := net.Listen("tcp", c.bindAddr)
if err != nil {
return err
}
// configure server
srv := &ssh.Server{
Addr: c.bindAddr,
Handler: func(s ssh.Session) { bastion.ShellHandler(s, Version, GitSha, GitTag, GitBranch) }, // ssh.Server.Handler is the handler for the DefaultSessionHandler
Version: fmt.Sprintf("sshportal-%s", Version),
ChannelHandlers: map[string]ssh.ChannelHandler{
"default": bastion.ChannelHandler,
},
}
// configure channel handler
bastion.DefaultChannelHandler = func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
switch newChan.ChannelType() {
case "session":
go ssh.DefaultSessionHandler(srv, conn, newChan, ctx)
case "direct-tcpip":
go ssh.DirectTCPIPHandler(srv, conn, newChan, ctx)
default:
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
log.Printf("failed to reject chan: %v", err)
}
}
}
if c.idleTimeout != 0 {
srv.IdleTimeout = c.idleTimeout
// gliderlabs/ssh requires MaxTimeout to be non-zero if we want to use IdleTimeout.
// So, set it to the max value, because we don't want a max timeout.
srv.MaxTimeout = math.MaxInt64
}
for _, opt := range []ssh.Option{
// custom PublicKeyAuth handler
ssh.PublicKeyAuth(bastion.PublicKeyAuthHandler(db, c.logsLocation, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
ssh.PasswordAuth(bastion.PasswordAuthHandler(db, c.logsLocation, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
// retrieve sshportal SSH private key from database
bastion.PrivateKeyFromDB(db, c.aesKey),
} {
if err := srv.SetOption(opt); err != nil {
return err
}
}
log.Printf("info: SSH Server accepting connections on %s, idle-timout=%v", c.bindAddr, c.idleTimeout)
return srv.Serve(ln)
}

View File

@@ -1,4 +1,5 @@
// +build !windows
package main
import (

20
util.go
View File

@@ -1,20 +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)
}
func wrapText(in string, length int) string {
if len(in) <= length {
return in
}
return in[0:length-3] + "..."
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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
}

View File

@@ -1,22 +0,0 @@
MIT License
Copyright (c) 2016-2017 Florian Bertholin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,54 +0,0 @@
package logchannel
import (
"encoding/binary"
"io"
"syscall"
"time"
"golang.org/x/crypto/ssh"
)
type logChannel struct {
channel ssh.Channel
writer io.WriteCloser
}
func writeTTYRecHeader(fd io.Writer, length int) {
t := time.Now()
tv := syscall.NsecToTimeval(t.UnixNano())
binary.Write(fd, binary.LittleEndian, int32(tv.Sec))
binary.Write(fd, binary.LittleEndian, int32(tv.Usec))
binary.Write(fd, binary.LittleEndian, int32(length))
}
func New(channel ssh.Channel, writer io.WriteCloser) *logChannel {
return &logChannel{
channel: channel,
writer: writer,
}
}
func (l *logChannel) Read(data []byte) (int, error) {
return l.Read(data)
}
func (l *logChannel) Write(data []byte) (int, error) {
writeTTYRecHeader(l.writer, len(data))
l.writer.Write(data)
return l.channel.Write(data)
}
func (l *logChannel) LogWrite(data []byte) (int, error) {
writeTTYRecHeader(l.writer, len(data))
return l.writer.Write(data)
}
func (l *logChannel) Close() error {
l.writer.Close()
return l.channel.Close()
}

View File

@@ -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.

View File

@@ -1,410 +0,0 @@
govalidator
===========
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/asaskevich/govalidator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![GoDoc](https://godoc.org/github.com/asaskevich/govalidator?status.png)](https://godoc.org/github.com/asaskevich/govalidator) [![Coverage Status](https://img.shields.io/coveralls/asaskevich/govalidator.svg)](https://coveralls.io/r/asaskevich/govalidator?branch=master) [![wercker status](https://app.wercker.com/status/1ec990b09ea86c910d5f08b0e02c6043/s "wercker status")](https://app.wercker.com/project/bykey/1ec990b09ea86c910d5f08b0e02c6043)
[![Build Status](https://travis-ci.org/asaskevich/govalidator.svg?branch=master)](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)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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)
)

View File

@@ -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'exRé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"},
}

View File

@@ -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
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 ./...

View File

@@ -1,21 +0,0 @@
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
<http://www.opensource.org/licenses/mit-license.php>

View File

@@ -1,92 +0,0 @@
# Humane Units [![Build Status](https://travis-ci.org/dustin/go-humanize.svg?branch=master)](https://travis-ci.org/dustin/go-humanize) [![GoDoc](https://godoc.org/github.com/dustin/go-humanize?status.svg)](https://godoc.org/github.com/dustin/go-humanize)
Just a few functions for helping humanize times and sizes.
`go get` it as `github.com/dustin/go-humanize`, import it as
`"github.com/dustin/go-humanize"`, use it as `humanize`
See [godoc](https://godoc.org/github.com/dustin/go-humanize) for
complete documentation.
## Sizes
This lets you take numbers like `82854982` and convert them to useful
strings like, `83MB` or `79MiB` (whichever you prefer).
Example:
```go
fmt.Printf("That file is %s.", humanize.Bytes(82854982))
```
## Times
This lets you take a `time.Time` and spit it out in relative terms.
For example, `12 seconds ago` or `3 days from now`.
Example:
```go
fmt.Printf("This was touched %s", humanize.Time(someTimeInstance))
```
Thanks to Kyle Lemons for the time implementation from an IRC
conversation one day. It's pretty neat.
## Ordinals
From a [mailing list discussion][odisc] where a user wanted to be able
to label ordinals.
0 -> 0th
1 -> 1st
2 -> 2nd
3 -> 3rd
4 -> 4th
[...]
Example:
```go
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193))
```
## Commas
Want to shove commas into numbers? Be my guest.
0 -> 0
100 -> 100
1000 -> 1,000
1000000000 -> 1,000,000,000
-100000 -> -100,000
Example:
```go
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491))
```
## Ftoa
Nicer float64 formatter that removes trailing zeros.
```go
fmt.Printf("%f", 2.24) // 2.240000
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
fmt.Printf("%f", 2.0) // 2.000000
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
```
## SI notation
Format numbers with [SI notation][sinotation].
Example:
```go
humanize.SI(0.00000000223, "M") // 2.23nM
```
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix

View File

@@ -1,31 +0,0 @@
package humanize
import (
"math/big"
)
// order of magnitude (to a max order)
func oomm(n, b *big.Int, maxmag int) (float64, int) {
mag := 0
m := &big.Int{}
for n.Cmp(b) >= 0 {
n.DivMod(n, b, m)
mag++
if mag == maxmag && maxmag >= 0 {
break
}
}
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
}
// total order of magnitude
// (same as above, but with no upper limit)
func oom(n, b *big.Int) (float64, int) {
mag := 0
m := &big.Int{}
for n.Cmp(b) >= 0 {
n.DivMod(n, b, m)
mag++
}
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
}

View File

@@ -1,173 +0,0 @@
package humanize
import (
"fmt"
"math/big"
"strings"
"unicode"
)
var (
bigIECExp = big.NewInt(1024)
// BigByte is one byte in bit.Ints
BigByte = big.NewInt(1)
// BigKiByte is 1,024 bytes in bit.Ints
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp)
// BigMiByte is 1,024 k bytes in bit.Ints
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp)
// BigGiByte is 1,024 m bytes in bit.Ints
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp)
// BigTiByte is 1,024 g bytes in bit.Ints
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp)
// BigPiByte is 1,024 t bytes in bit.Ints
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp)
// BigEiByte is 1,024 p bytes in bit.Ints
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp)
// BigZiByte is 1,024 e bytes in bit.Ints
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp)
// BigYiByte is 1,024 z bytes in bit.Ints
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp)
)
var (
bigSIExp = big.NewInt(1000)
// BigSIByte is one SI byte in big.Ints
BigSIByte = big.NewInt(1)
// BigKByte is 1,000 SI bytes in big.Ints
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp)
// BigMByte is 1,000 SI k bytes in big.Ints
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp)
// BigGByte is 1,000 SI m bytes in big.Ints
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp)
// BigTByte is 1,000 SI g bytes in big.Ints
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp)
// BigPByte is 1,000 SI t bytes in big.Ints
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp)
// BigEByte is 1,000 SI p bytes in big.Ints
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp)
// BigZByte is 1,000 SI e bytes in big.Ints
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp)
// BigYByte is 1,000 SI z bytes in big.Ints
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp)
)
var bigBytesSizeTable = map[string]*big.Int{
"b": BigByte,
"kib": BigKiByte,
"kb": BigKByte,
"mib": BigMiByte,
"mb": BigMByte,
"gib": BigGiByte,
"gb": BigGByte,
"tib": BigTiByte,
"tb": BigTByte,
"pib": BigPiByte,
"pb": BigPByte,
"eib": BigEiByte,
"eb": BigEByte,
"zib": BigZiByte,
"zb": BigZByte,
"yib": BigYiByte,
"yb": BigYByte,
// Without suffix
"": BigByte,
"ki": BigKiByte,
"k": BigKByte,
"mi": BigMiByte,
"m": BigMByte,
"gi": BigGiByte,
"g": BigGByte,
"ti": BigTiByte,
"t": BigTByte,
"pi": BigPiByte,
"p": BigPByte,
"ei": BigEiByte,
"e": BigEByte,
"z": BigZByte,
"zi": BigZiByte,
"y": BigYByte,
"yi": BigYiByte,
}
var ten = big.NewInt(10)
func humanateBigBytes(s, base *big.Int, sizes []string) string {
if s.Cmp(ten) < 0 {
return fmt.Sprintf("%d B", s)
}
c := (&big.Int{}).Set(s)
val, mag := oomm(c, base, len(sizes)-1)
suffix := sizes[mag]
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
// BigBytes produces a human readable representation of an SI size.
//
// See also: ParseBigBytes.
//
// BigBytes(82854982) -> 83MB
func BigBytes(s *big.Int) string {
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
return humanateBigBytes(s, bigSIExp, sizes)
}
// BigIBytes produces a human readable representation of an IEC size.
//
// See also: ParseBigBytes.
//
// BigIBytes(82854982) -> 79MiB
func BigIBytes(s *big.Int) string {
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
return humanateBigBytes(s, bigIECExp, sizes)
}
// ParseBigBytes parses a string representation of bytes into the number
// of bytes it represents.
//
// See also: BigBytes, BigIBytes.
//
// ParseBigBytes("42MB") -> 42000000, nil
// ParseBigBytes("42mib") -> 44040192, nil
func ParseBigBytes(s string) (*big.Int, error) {
lastDigit := 0
hasComma := false
for _, r := range s {
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
break
}
if r == ',' {
hasComma = true
}
lastDigit++
}
num := s[:lastDigit]
if hasComma {
num = strings.Replace(num, ",", "", -1)
}
val := &big.Rat{}
_, err := fmt.Sscanf(num, "%f", val)
if err != nil {
return nil, err
}
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
if m, ok := bigBytesSizeTable[extra]; ok {
mv := (&big.Rat{}).SetInt(m)
val.Mul(val, mv)
rv := &big.Int{}
rv.Div(val.Num(), val.Denom())
return rv, nil
}
return nil, fmt.Errorf("unhandled size name: %v", extra)
}

View File

@@ -1,143 +0,0 @@
package humanize
import (
"fmt"
"math"
"strconv"
"strings"
"unicode"
)
// IEC Sizes.
// kibis of bits
const (
Byte = 1 << (iota * 10)
KiByte
MiByte
GiByte
TiByte
PiByte
EiByte
)
// SI Sizes.
const (
IByte = 1
KByte = IByte * 1000
MByte = KByte * 1000
GByte = MByte * 1000
TByte = GByte * 1000
PByte = TByte * 1000
EByte = PByte * 1000
)
var bytesSizeTable = map[string]uint64{
"b": Byte,
"kib": KiByte,
"kb": KByte,
"mib": MiByte,
"mb": MByte,
"gib": GiByte,
"gb": GByte,
"tib": TiByte,
"tb": TByte,
"pib": PiByte,
"pb": PByte,
"eib": EiByte,
"eb": EByte,
// Without suffix
"": Byte,
"ki": KiByte,
"k": KByte,
"mi": MiByte,
"m": MByte,
"gi": GiByte,
"g": GByte,
"ti": TiByte,
"t": TByte,
"pi": PiByte,
"p": PByte,
"ei": EiByte,
"e": EByte,
}
func logn(n, b float64) float64 {
return math.Log(n) / math.Log(b)
}
func humanateBytes(s uint64, base float64, sizes []string) string {
if s < 10 {
return fmt.Sprintf("%d B", s)
}
e := math.Floor(logn(float64(s), base))
suffix := sizes[int(e)]
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
// Bytes produces a human readable representation of an SI size.
//
// See also: ParseBytes.
//
// Bytes(82854982) -> 83MB
func Bytes(s uint64) string {
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
return humanateBytes(s, 1000, sizes)
}
// IBytes produces a human readable representation of an IEC size.
//
// See also: ParseBytes.
//
// IBytes(82854982) -> 79MiB
func IBytes(s uint64) string {
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
return humanateBytes(s, 1024, sizes)
}
// ParseBytes parses a string representation of bytes into the number
// of bytes it represents.
//
// See Also: Bytes, IBytes.
//
// ParseBytes("42MB") -> 42000000, nil
// ParseBytes("42mib") -> 44040192, nil
func ParseBytes(s string) (uint64, error) {
lastDigit := 0
hasComma := false
for _, r := range s {
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
break
}
if r == ',' {
hasComma = true
}
lastDigit++
}
num := s[:lastDigit]
if hasComma {
num = strings.Replace(num, ",", "", -1)
}
f, err := strconv.ParseFloat(num, 64)
if err != nil {
return 0, err
}
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
if m, ok := bytesSizeTable[extra]; ok {
f *= float64(m)
if f >= math.MaxUint64 {
return 0, fmt.Errorf("too large: %v", s)
}
return uint64(f), nil
}
return 0, fmt.Errorf("unhandled size name: %v", extra)
}

View File

@@ -1,108 +0,0 @@
package humanize
import (
"bytes"
"math"
"math/big"
"strconv"
"strings"
)
// Comma produces a string form of the given number in base 10 with
// commas after every three orders of magnitude.
//
// e.g. Comma(834142) -> 834,142
func Comma(v int64) string {
sign := ""
// minin64 can't be negated to a usable value, so it has to be special cased.
if v == math.MinInt64 {
return "-9,223,372,036,854,775,808"
}
if v < 0 {
sign = "-"
v = 0 - v
}
parts := []string{"", "", "", "", "", "", ""}
j := len(parts) - 1
for v > 999 {
parts[j] = strconv.FormatInt(v%1000, 10)
switch len(parts[j]) {
case 2:
parts[j] = "0" + parts[j]
case 1:
parts[j] = "00" + parts[j]
}
v = v / 1000
j--
}
parts[j] = strconv.Itoa(int(v))
return sign + strings.Join(parts[j:], ",")
}
// Commaf produces a string form of the given number in base 10 with
// commas after every three orders of magnitude.
//
// e.g. Commaf(834142.32) -> 834,142.32
func Commaf(v float64) string {
buf := &bytes.Buffer{}
if v < 0 {
buf.Write([]byte{'-'})
v = 0 - v
}
comma := []byte{','}
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".")
pos := 0
if len(parts[0])%3 != 0 {
pos += len(parts[0]) % 3
buf.WriteString(parts[0][:pos])
buf.Write(comma)
}
for ; pos < len(parts[0]); pos += 3 {
buf.WriteString(parts[0][pos : pos+3])
buf.Write(comma)
}
buf.Truncate(buf.Len() - 1)
if len(parts) > 1 {
buf.Write([]byte{'.'})
buf.WriteString(parts[1])
}
return buf.String()
}
// BigComma produces a string form of the given big.Int in base 10
// with commas after every three orders of magnitude.
func BigComma(b *big.Int) string {
sign := ""
if b.Sign() < 0 {
sign = "-"
b.Abs(b)
}
athousand := big.NewInt(1000)
c := (&big.Int{}).Set(b)
_, m := oom(c, athousand)
parts := make([]string, m+1)
j := len(parts) - 1
mod := &big.Int{}
for b.Cmp(athousand) >= 0 {
b.DivMod(b, athousand, mod)
parts[j] = strconv.FormatInt(mod.Int64(), 10)
switch len(parts[j]) {
case 2:
parts[j] = "0" + parts[j]
case 1:
parts[j] = "00" + parts[j]
}
j--
}
parts[j] = strconv.Itoa(int(b.Int64()))
return sign + strings.Join(parts[j:], ",")
}

View File

@@ -1,40 +0,0 @@
// +build go1.6
package humanize
import (
"bytes"
"math/big"
"strings"
)
// BigCommaf produces a string form of the given big.Float in base 10
// with commas after every three orders of magnitude.
func BigCommaf(v *big.Float) string {
buf := &bytes.Buffer{}
if v.Sign() < 0 {
buf.Write([]byte{'-'})
v.Abs(v)
}
comma := []byte{','}
parts := strings.Split(v.Text('f', -1), ".")
pos := 0
if len(parts[0])%3 != 0 {
pos += len(parts[0]) % 3
buf.WriteString(parts[0][:pos])
buf.Write(comma)
}
for ; pos < len(parts[0]); pos += 3 {
buf.WriteString(parts[0][pos : pos+3])
buf.Write(comma)
}
buf.Truncate(buf.Len() - 1)
if len(parts) > 1 {
buf.Write([]byte{'.'})
buf.WriteString(parts[1])
}
return buf.String()
}

View File

@@ -1,23 +0,0 @@
package humanize
import "strconv"
func stripTrailingZeros(s string) string {
offset := len(s) - 1
for offset > 0 {
if s[offset] == '.' {
offset--
break
}
if s[offset] != '0' {
break
}
offset--
}
return s[:offset+1]
}
// Ftoa converts a float to a string with no trailing zeros.
func Ftoa(num float64) string {
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64))
}

View File

@@ -1,8 +0,0 @@
/*
Package humanize converts boring ugly numbers to human-friendly strings and back.
Durations can be turned into strings such as "3 days ago", numbers
representing sizes like 82854982 into useful strings like, "83MB" or
"79MiB" (whichever you prefer).
*/
package humanize

View File

@@ -1,192 +0,0 @@
package humanize
/*
Slightly adapted from the source to fit go-humanize.
Author: https://github.com/gorhill
Source: https://gist.github.com/gorhill/5285193
*/
import (
"math"
"strconv"
)
var (
renderFloatPrecisionMultipliers = [...]float64{
1,
10,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000,
1000000000,
}
renderFloatPrecisionRounders = [...]float64{
0.5,
0.05,
0.005,
0.0005,
0.00005,
0.000005,
0.0000005,
0.00000005,
0.000000005,
0.0000000005,
}
)
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
// * thousands separator
// * decimal separator
// * decimal precision
//
// Usage: s := RenderFloat(format, n)
// The format parameter tells how to render the number n.
//
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
//
// Examples of format strings, given n = 12345.6789:
// "#,###.##" => "12,345.67"
// "#,###." => "12,345"
// "#,###" => "12345,678"
// "#\u202F###,##" => "12345,68"
// "#.###,###### => 12.345,678900
// "" (aka default format) => 12,345.67
//
// The highest precision allowed is 9 digits after the decimal symbol.
// There is also a version for integer number, FormatInteger(),
// which is convenient for calls within template.
func FormatFloat(format string, n float64) string {
// Special cases:
// NaN = "NaN"
// +Inf = "+Infinity"
// -Inf = "-Infinity"
if math.IsNaN(n) {
return "NaN"
}
if n > math.MaxFloat64 {
return "Infinity"
}
if n < -math.MaxFloat64 {
return "-Infinity"
}
// default format
precision := 2
decimalStr := "."
thousandStr := ","
positiveStr := ""
negativeStr := "-"
if len(format) > 0 {
format := []rune(format)
// If there is an explicit format directive,
// then default values are these:
precision = 9
thousandStr = ""
// collect indices of meaningful formatting directives
formatIndx := []int{}
for i, char := range format {
if char != '#' && char != '0' {
formatIndx = append(formatIndx, i)
}
}
if len(formatIndx) > 0 {
// Directive at index 0:
// Must be a '+'
// Raise an error if not the case
// index: 0123456789
// +0.000,000
// +000,000.0
// +0000.00
// +0000
if formatIndx[0] == 0 {
if format[formatIndx[0]] != '+' {
panic("RenderFloat(): invalid positive sign directive")
}
positiveStr = "+"
formatIndx = formatIndx[1:]
}
// Two directives:
// First is thousands separator
// Raise an error if not followed by 3-digit
// 0123456789
// 0.000,000
// 000,000.00
if len(formatIndx) == 2 {
if (formatIndx[1] - formatIndx[0]) != 4 {
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
}
thousandStr = string(format[formatIndx[0]])
formatIndx = formatIndx[1:]
}
// One directive:
// Directive is decimal separator
// The number of digit-specifier following the separator indicates wanted precision
// 0123456789
// 0.00
// 000,0000
if len(formatIndx) == 1 {
decimalStr = string(format[formatIndx[0]])
precision = len(format) - formatIndx[0] - 1
}
}
}
// generate sign part
var signStr string
if n >= 0.000000001 {
signStr = positiveStr
} else if n <= -0.000000001 {
signStr = negativeStr
n = -n
} else {
signStr = ""
n = 0.0
}
// split number into integer and fractional parts
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
// generate integer part string
intStr := strconv.FormatInt(int64(intf), 10)
// add thousand separator if required
if len(thousandStr) > 0 {
for i := len(intStr); i > 3; {
i -= 3
intStr = intStr[:i] + thousandStr + intStr[i:]
}
}
// no fractional part, we can leave now
if precision == 0 {
return signStr + intStr
}
// generate fractional part
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
// may need padding
if len(fracStr) < precision {
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
}
return signStr + intStr + decimalStr + fracStr
}
// FormatInteger produces a formatted number as string.
// See FormatFloat.
func FormatInteger(format string, n int) string {
return FormatFloat(format, float64(n))
}

View File

@@ -1,25 +0,0 @@
package humanize
import "strconv"
// Ordinal gives you the input number in a rank/ordinal format.
//
// Ordinal(3) -> 3rd
func Ordinal(x int) string {
suffix := "th"
switch x % 10 {
case 1:
if x%100 != 11 {
suffix = "st"
}
case 2:
if x%100 != 12 {
suffix = "nd"
}
case 3:
if x%100 != 13 {
suffix = "rd"
}
}
return strconv.Itoa(x) + suffix
}

View File

@@ -1,113 +0,0 @@
package humanize
import (
"errors"
"math"
"regexp"
"strconv"
)
var siPrefixTable = map[float64]string{
-24: "y", // yocto
-21: "z", // zepto
-18: "a", // atto
-15: "f", // femto
-12: "p", // pico
-9: "n", // nano
-6: "µ", // micro
-3: "m", // milli
0: "",
3: "k", // kilo
6: "M", // mega
9: "G", // giga
12: "T", // tera
15: "P", // peta
18: "E", // exa
21: "Z", // zetta
24: "Y", // yotta
}
var revSIPrefixTable = revfmap(siPrefixTable)
// revfmap reverses the map and precomputes the power multiplier
func revfmap(in map[float64]string) map[string]float64 {
rv := map[string]float64{}
for k, v := range in {
rv[v] = math.Pow(10, k)
}
return rv
}
var riParseRegex *regexp.Regexp
func init() {
ri := `^([\-0-9.]+)\s?([`
for _, v := range siPrefixTable {
ri += v
}
ri += `]?)(.*)`
riParseRegex = regexp.MustCompile(ri)
}
// ComputeSI finds the most appropriate SI prefix for the given number
// and returns the prefix along with the value adjusted to be within
// that prefix.
//
// See also: SI, ParseSI.
//
// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p")
func ComputeSI(input float64) (float64, string) {
if input == 0 {
return 0, ""
}
mag := math.Abs(input)
exponent := math.Floor(logn(mag, 10))
exponent = math.Floor(exponent/3) * 3
value := mag / math.Pow(10, exponent)
// Handle special case where value is exactly 1000.0
// Should return 1M instead of 1000k
if value == 1000.0 {
exponent += 3
value = mag / math.Pow(10, exponent)
}
value = math.Copysign(value, input)
prefix := siPrefixTable[exponent]
return value, prefix
}
// SI returns a string with default formatting.
//
// SI uses Ftoa to format float value, removing trailing zeros.
//
// See also: ComputeSI, ParseSI.
//
// e.g. SI(1000000, B) -> 1MB
// e.g. SI(2.2345e-12, "F") -> 2.2345pF
func SI(input float64, unit string) string {
value, prefix := ComputeSI(input)
return Ftoa(value) + " " + prefix + unit
}
var errInvalid = errors.New("invalid input")
// ParseSI parses an SI string back into the number and unit.
//
// See also: SI, ComputeSI.
//
// e.g. ParseSI(2.2345pF) -> (2.2345e-12, "F", nil)
func ParseSI(input string) (float64, string, error) {
found := riParseRegex.FindStringSubmatch(input)
if len(found) != 4 {
return 0, "", errInvalid
}
mag := revSIPrefixTable[found[2]]
unit := found[3]
base, err := strconv.ParseFloat(found[1], 64)
return base * mag, unit, err
}

View File

@@ -1,117 +0,0 @@
package humanize
import (
"fmt"
"math"
"sort"
"time"
)
// Seconds-based time units
const (
Day = 24 * time.Hour
Week = 7 * Day
Month = 30 * Day
Year = 12 * Month
LongTime = 37 * Year
)
// Time formats a time into a relative string.
//
// Time(someT) -> "3 weeks ago"
func Time(then time.Time) string {
return RelTime(then, time.Now(), "ago", "from now")
}
// A RelTimeMagnitude struct contains a relative time point at which
// the relative format of time will switch to a new format string. A
// slice of these in ascending order by their "D" field is passed to
// CustomRelTime to format durations.
//
// The Format field is a string that may contain a "%s" which will be
// replaced with the appropriate signed label (e.g. "ago" or "from
// now") and a "%d" that will be replaced by the quantity.
//
// The DivBy field is the amount of time the time difference must be
// divided by in order to display correctly.
//
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
// DivBy should be time.Minute so whatever the duration is will be
// expressed in minutes.
type RelTimeMagnitude struct {
D time.Duration
Format string
DivBy time.Duration
}
var defaultMagnitudes = []RelTimeMagnitude{
{time.Second, "now", time.Second},
{2 * time.Second, "1 second %s", 1},
{time.Minute, "%d seconds %s", time.Second},
{2 * time.Minute, "1 minute %s", 1},
{time.Hour, "%d minutes %s", time.Minute},
{2 * time.Hour, "1 hour %s", 1},
{Day, "%d hours %s", time.Hour},
{2 * Day, "1 day %s", 1},
{Week, "%d days %s", Day},
{2 * Week, "1 week %s", 1},
{Month, "%d weeks %s", Week},
{2 * Month, "1 month %s", 1},
{Year, "%d months %s", Month},
{18 * Month, "1 year %s", 1},
{2 * Year, "2 years %s", 1},
{LongTime, "%d years %s", Year},
{math.MaxInt64, "a long while %s", 1},
}
// RelTime formats a time into a relative string.
//
// It takes two times and two labels. In addition to the generic time
// delta string (e.g. 5 minutes), the labels are used applied so that
// the label corresponding to the smaller time is applied.
//
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
func RelTime(a, b time.Time, albl, blbl string) string {
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
}
// CustomRelTime formats a time into a relative string.
//
// It takes two times two labels and a table of relative time formats.
// In addition to the generic time delta string (e.g. 5 minutes), the
// labels are used applied so that the label corresponding to the
// smaller time is applied.
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
lbl := albl
diff := b.Sub(a)
if a.After(b) {
lbl = blbl
diff = a.Sub(b)
}
n := sort.Search(len(magnitudes), func(i int) bool {
return magnitudes[i].D >= diff
})
if n >= len(magnitudes) {
n = len(magnitudes) - 1
}
mag := magnitudes[n]
args := []interface{}{}
escaped := false
for _, ch := range mag.Format {
if escaped {
switch ch {
case 's':
args = append(args, lbl)
case 'd':
args = append(args, diff/mag.DivBy)
}
escaped = false
} else {
escaped = ch == '%'
}
}
return fmt.Sprintf(mag.Format, args...)
}

View File

@@ -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.

View File

@@ -1,96 +0,0 @@
# gliderlabs/ssh
[![GoDoc](https://godoc.org/github.com/gliderlabs/ssh?status.svg)](https://godoc.org/github.com/gliderlabs/ssh)
[![CircleCI](https://img.shields.io/circleci/project/github/gliderlabs/ssh.svg)](https://circleci.com/gh/gliderlabs/ssh)
[![Go Report Card](https://goreportcard.com/badge/github.com/gliderlabs/ssh)](https://goreportcard.com/report/github.com/gliderlabs/ssh)
[![OpenCollective](https://opencollective.com/ssh/sponsors/badge.svg)](#sponsors)
[![Slack](http://slack.gliderlabs.com/badge.svg)](http://slack.gliderlabs.com)
[![Email Updates](https://img.shields.io/badge/updates-subscribe-yellow.svg)](https://app.convertkit.com/landing_pages/243312)
> The Glider Labs SSH server package is dope. &mdash;[@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:
```go
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

View File

@@ -1,83 +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"}
// SetAgentRequested sets up the session context so that AgentRequested
// returns true.
func SetAgentRequested(ctx Context) {
ctx.SetValue(contextKeyAgentRequest, true)
}
// AgentRequested returns true if the client requested agent forwarding.
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)
}
}

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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 applyConnMetadata(ctx Context, 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)
}

View File

@@ -1,47 +0,0 @@
/*
Package ssh wraps the crypto/ssh package with a higher-level API for building
SSH servers. The goal of the API was to make it as simple as using net/http, so
the API is very similar.
You should be able to build any SSH server using only this package, which wraps
relevant types and some functions from crypto/ssh. However, you still need to
use crypto/ssh for building SSH clients.
ListenAndServe starts an SSH server with a given address, handler, and options. The
handler is usually nil, which means to use DefaultHandler. Handle sets DefaultHandler:
ssh.Handle(func(s ssh.Session) {
io.WriteString(s, "Hello world\n")
})
log.Fatal(ssh.ListenAndServe(":2222", nil))
If you don't specify a host key, it will generate one every time. This is convenient
except you'll have to deal with clients being confused that the host key is different.
It's a better idea to generate or point to an existing key on your system:
log.Fatal(ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile("/Users/progrium/.ssh/id_rsa")))
Although all options have functional option helpers, another way to control the
server's behavior is by creating a custom Server:
s := &ssh.Server{
Addr: ":2222",
Handler: sessionHandler,
PublicKeyHandler: authHandler,
}
s.AddHostKey(hostKeySigner)
log.Fatal(s.ListenAndServe())
This package automatically handles basic SSH requests like setting environment
variables, requesting PTY, and changing window size. These requests are
processed, responded to, and any relevant state is updated. This state is then
exposed to you via the Session interface.
The one big feature missing from the Session abstraction is signals. This was
started, but not completed. Pull Requests welcome!
*/
package ssh

View File

@@ -1,77 +0,0 @@
package ssh
import (
"io/ioutil"
gossh "golang.org/x/crypto/ssh"
)
// PasswordAuth returns a functional option that sets PasswordHandler on the server.
func PasswordAuth(fn PasswordHandler) Option {
return func(srv *Server) error {
srv.PasswordHandler = fn
return nil
}
}
// PublicKeyAuth returns a functional option that sets PublicKeyHandler on the server.
func PublicKeyAuth(fn PublicKeyHandler) Option {
return func(srv *Server) error {
srv.PublicKeyHandler = fn
return nil
}
}
// HostKeyFile returns a functional option that adds HostSigners to the server
// from a PEM file at filepath.
func HostKeyFile(filepath string) Option {
return func(srv *Server) error {
pemBytes, err := ioutil.ReadFile(filepath)
if err != nil {
return err
}
signer, err := gossh.ParsePrivateKey(pemBytes)
if err != nil {
return err
}
srv.AddHostKey(signer)
return nil
}
}
// HostKeyPEM returns a functional option that adds HostSigners to the server
// from a PEM file as bytes.
func HostKeyPEM(bytes []byte) Option {
return func(srv *Server) error {
signer, err := gossh.ParsePrivateKey(bytes)
if err != nil {
return err
}
srv.AddHostKey(signer)
return nil
}
}
// NoPty returns a functional option that sets PtyCallback to return false,
// denying PTY requests.
func NoPty() Option {
return func(srv *Server) error {
srv.PtyCallback = func(ctx Context, pty Pty) bool {
return false
}
return nil
}
}
// WrapConn returns a functional option that sets ConnCallback on the server.
func WrapConn(fn ConnCallback) Option {
return func(srv *Server) error {
srv.ConnCallback = fn
return nil
}
}

View File

@@ -1,336 +0,0 @@
package ssh
import (
"context"
"errors"
"fmt"
"net"
"sync"
"time"
gossh "golang.org/x/crypto/ssh"
)
// ErrServerClosed is returned by the Server's Serve, ListenAndServe,
// and ListenAndServeTLS methods after a call to Shutdown or Close.
var ErrServerClosed = errors.New("ssh: Server closed")
// Server defines parameters for running an SSH server. The zero value for
// Server is a valid configuration. When both PasswordHandler and
// PublicKeyHandler are nil, no client authentication is performed.
type Server struct {
Addr string // TCP address to listen on, ":22" if empty
Handler Handler // handler to invoke, ssh.DefaultHandler if nil
HostSigners []Signer // private keys for the host key, must have at least one
Version string // server version to be sent before the initial handshake
PasswordHandler PasswordHandler // password authentication handler
PublicKeyHandler PublicKeyHandler // public key authentication handler
ChannelHandler ChannelHandler // channel handler
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
IdleTimeout time.Duration // connection timeout when no activity, none if empty
MaxTimeout time.Duration // absolute connection timeout, none if empty
mu sync.Mutex
listeners map[net.Listener]struct{}
conns map[*gossh.ServerConn]struct{}
doneChan chan struct{}
}
type ChannelHandler func(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context)
func (srv *Server) ensureHostSigner() error {
if len(srv.HostSigners) == 0 {
signer, err := generateSigner()
if err != nil {
return err
}
srv.HostSigners = append(srv.HostSigners, signer)
}
return nil
}
func (srv *Server) config(ctx Context) *gossh.ServerConfig {
config := &gossh.ServerConfig{}
for _, signer := range srv.HostSigners {
config.AddHostKey(signer)
}
if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil {
config.NoClientAuth = true
}
if srv.Version != "" {
config.ServerVersion = "SSH-2.0-" + srv.Version
}
if srv.PasswordHandler != nil {
config.PasswordCallback = func(conn gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) {
applyConnMetadata(ctx, conn)
if ok := srv.PasswordHandler(ctx, string(password)); !ok {
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
}
return ctx.Permissions().Permissions, nil
}
}
if srv.PublicKeyHandler != nil {
config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
applyConnMetadata(ctx, conn)
if ok := srv.PublicKeyHandler(ctx, key); !ok {
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
}
ctx.SetValue(ContextKeyPublicKey, key)
return ctx.Permissions().Permissions, nil
}
}
return config
}
// Handle sets the Handler for the server.
func (srv *Server) Handle(fn Handler) {
srv.Handler = fn
}
// Close immediately closes all active listeners and all active
// connections.
//
// Close returns any error returned from closing the Server's
// underlying Listener(s).
func (srv *Server) Close() error {
srv.mu.Lock()
defer srv.mu.Unlock()
srv.closeDoneChanLocked()
err := srv.closeListenersLocked()
for c := range srv.conns {
c.Close()
delete(srv.conns, c)
}
return err
}
// shutdownPollInterval is how often we poll for quiescence
// during Server.Shutdown. This is lower during tests, to
// speed up tests.
// Ideally we could find a solution that doesn't involve polling,
// but which also doesn't have a high runtime cost (and doesn't
// involve any contentious mutexes), but that is left as an
// exercise for the reader.
var shutdownPollInterval = 500 * time.Millisecond
// Shutdown gracefully shuts down the server without interrupting any
// active connections. Shutdown works by first closing all open
// listeners, and then waiting indefinitely for connections to close.
// If the provided context expires before the shutdown is complete,
// then the context's error is returned.
func (srv *Server) Shutdown(ctx context.Context) error {
srv.mu.Lock()
lnerr := srv.closeListenersLocked()
srv.closeDoneChanLocked()
srv.mu.Unlock()
ticker := time.NewTicker(shutdownPollInterval)
defer ticker.Stop()
for {
srv.mu.Lock()
conns := len(srv.conns)
srv.mu.Unlock()
if conns == 0 {
return lnerr
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
// Serve accepts incoming connections on the Listener l, creating a new
// connection goroutine for each. The connection goroutines read requests and then
// calls srv.Handler to handle sessions.
//
// Serve always returns a non-nil error.
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
if err := srv.ensureHostSigner(); err != nil {
return err
}
if srv.Handler == nil {
srv.Handler = DefaultHandler
}
var tempDelay time.Duration
srv.trackListener(l, true)
defer srv.trackListener(l, false)
for {
conn, e := l.Accept()
if e != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
time.Sleep(tempDelay)
continue
}
return e
}
go srv.handleConn(conn)
}
}
func (srv *Server) handleConn(newConn net.Conn) {
if srv.ConnCallback != nil {
cbConn := srv.ConnCallback(newConn)
if cbConn == nil {
newConn.Close()
return
}
newConn = cbConn
}
ctx, cancel := newContext(srv)
conn := &serverConn{
Conn: newConn,
idleTimeout: srv.IdleTimeout,
closeCanceler: cancel,
}
if srv.MaxTimeout > 0 {
conn.maxDeadline = time.Now().Add(srv.MaxTimeout)
}
defer conn.Close()
sshConn, chans, reqs, err := gossh.NewServerConn(conn, srv.config(ctx))
if err != nil {
// TODO: trigger event callback
return
}
srv.trackConn(sshConn, true)
defer srv.trackConn(sshConn, false)
ctx.SetValue(ContextKeyConn, sshConn)
applyConnMetadata(ctx, sshConn)
go gossh.DiscardRequests(reqs)
for ch := range chans {
if srv.ChannelHandler == nil {
DefaultChannelHandler(srv, sshConn, ch, ctx)
} else {
srv.ChannelHandler(srv, sshConn, ch, ctx)
}
}
}
func DefaultChannelHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
switch newChan.ChannelType() {
case "session":
go sessionHandler(srv, conn, newChan, ctx)
case "direct-tcpip":
go directTcpipHandler(srv, conn, newChan, ctx)
default:
newChan.Reject(gossh.UnknownChannelType, "unsupported channel type")
}
}
// ListenAndServe listens on the TCP network address srv.Addr and then calls
// Serve to handle incoming connections. If srv.Addr is blank, ":22" is used.
// ListenAndServe always returns a non-nil error.
func (srv *Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":22"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
// AddHostKey adds a private key as a host key. If an existing host key exists
// with the same algorithm, it is overwritten. Each server config must have at
// least one host key.
func (srv *Server) AddHostKey(key Signer) {
// these are later added via AddHostKey on ServerConfig, which performs the
// check for one of every algorithm.
srv.HostSigners = append(srv.HostSigners, key)
}
// SetOption runs a functional option against the server.
func (srv *Server) SetOption(option Option) error {
return option(srv)
}
func (srv *Server) getDoneChan() <-chan struct{} {
srv.mu.Lock()
defer srv.mu.Unlock()
return srv.getDoneChanLocked()
}
func (srv *Server) getDoneChanLocked() chan struct{} {
if srv.doneChan == nil {
srv.doneChan = make(chan struct{})
}
return srv.doneChan
}
func (srv *Server) closeDoneChanLocked() {
ch := srv.getDoneChanLocked()
select {
case <-ch:
// Already closed. Don't close again.
default:
// Safe to close here. We're the only closer, guarded
// by srv.mu.
close(ch)
}
}
func (srv *Server) closeListenersLocked() error {
var err error
for ln := range srv.listeners {
if cerr := ln.Close(); cerr != nil && err == nil {
err = cerr
}
delete(srv.listeners, ln)
}
return err
}
func (srv *Server) trackListener(ln net.Listener, add bool) {
srv.mu.Lock()
defer srv.mu.Unlock()
if srv.listeners == nil {
srv.listeners = make(map[net.Listener]struct{})
}
if add {
// If the *Server is being reused after a previous
// Close or Shutdown, reset its doneChan:
if len(srv.listeners) == 0 && len(srv.conns) == 0 {
srv.doneChan = nil
}
srv.listeners[ln] = struct{}{}
} else {
delete(srv.listeners, ln)
}
}
func (srv *Server) trackConn(c *gossh.ServerConn, add bool) {
srv.mu.Lock()
defer srv.mu.Unlock()
if srv.conns == nil {
srv.conns = make(map[*gossh.ServerConn]struct{})
}
if add {
srv.conns[c] = struct{}{}
} else {
delete(srv.conns, c)
}
}

View File

@@ -1,287 +0,0 @@
package ssh
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"sync"
"github.com/anmitsu/go-shlex"
gossh "golang.org/x/crypto/ssh"
)
// Session provides access to information about an SSH session and methods
// to read and write to the SSH channel with an embedded Channel interface from
// cypto/ssh.
//
// When Command() returns an empty slice, the user requested a shell. Otherwise
// the user is performing an exec with those command arguments.
//
// TODO: Signals
type Session interface {
gossh.Channel
// User returns the username used when establishing the SSH connection.
User() string
// RemoteAddr returns the net.Addr of the client side of the connection.
RemoteAddr() net.Addr
// LocalAddr returns the net.Addr of the server side of the connection.
LocalAddr() net.Addr
// Environ returns a copy of strings representing the environment set by the
// user for this session, in the form "key=value".
Environ() []string
// Exit sends an exit status and then closes the session.
Exit(code int) error
// Command returns a shell parsed slice of arguments that were provided by the
// user. Shell parsing splits the command string according to POSIX shell rules,
// which considers quoting not just whitespace.
Command() []string
// PublicKey returns the PublicKey used to authenticate. If a public key was not
// used it will return nil.
PublicKey() PublicKey
// Context returns the connection's context. The returned context is always
// non-nil and holds the same data as the Context passed into auth
// handlers and callbacks.
//
// The context is canceled when the client's connection closes or I/O
// operation fails.
Context() context.Context
// Permissions returns a copy of the Permissions object that was available for
// setup in the auth handlers via the Context.
Permissions() Permissions
// Pty returns PTY information, a channel of window size changes, and a boolean
// of whether or not a PTY was accepted for this session.
Pty() (Pty, <-chan Window, bool)
// Signals registers a channel to receive signals sent from the client. The
// channel must handle signal sends or it will block the SSH request loop.
// Registering nil will unregister the channel from signal sends. During the
// time no channel is registered signals are buffered up to a reasonable amount.
// If there are buffered signals when a channel is registered, they will be
// sent in order on the channel immediately after registering.
Signals(c chan<- Signal)
}
// maxSigBufSize is how many signals will be buffered
// when there is no signal channel specified
const maxSigBufSize = 128
func sessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
ch, reqs, err := newChan.Accept()
if err != nil {
// TODO: trigger event callback
return
}
sess := &session{
Channel: ch,
conn: conn,
handler: srv.Handler,
ptyCb: srv.PtyCallback,
ctx: ctx,
}
sess.handleRequests(reqs)
}
type session struct {
sync.Mutex
gossh.Channel
conn *gossh.ServerConn
handler Handler
handled bool
exited bool
pty *Pty
winch chan Window
env []string
ptyCb PtyCallback
cmd []string
ctx Context
sigCh chan<- Signal
sigBuf []Signal
}
func (sess *session) Write(p []byte) (n int, err error) {
if sess.pty != nil {
m := len(p)
// normalize \n to \r\n when pty is accepted.
// this is a hardcoded shortcut since we don't support terminal modes.
p = bytes.Replace(p, []byte{'\n'}, []byte{'\r', '\n'}, -1)
p = bytes.Replace(p, []byte{'\r', '\r', '\n'}, []byte{'\r', '\n'}, -1)
n, err = sess.Channel.Write(p)
if n > m {
n = m
}
return
}
return sess.Channel.Write(p)
}
func (sess *session) PublicKey() PublicKey {
sessionkey := sess.ctx.Value(ContextKeyPublicKey)
if sessionkey == nil {
return nil
}
return sessionkey.(PublicKey)
}
func (sess *session) Permissions() Permissions {
// use context permissions because its properly
// wrapped and easier to dereference
perms := sess.ctx.Value(ContextKeyPermissions).(*Permissions)
return *perms
}
func (sess *session) Context() context.Context {
return sess.ctx
}
func (sess *session) Exit(code int) error {
sess.Lock()
defer sess.Unlock()
if sess.exited {
return errors.New("Session.Exit called multiple times")
}
sess.exited = true
status := struct{ Status uint32 }{uint32(code)}
_, err := sess.SendRequest("exit-status", false, gossh.Marshal(&status))
if err != nil {
return err
}
return sess.Close()
}
func (sess *session) User() string {
return sess.conn.User()
}
func (sess *session) RemoteAddr() net.Addr {
return sess.conn.RemoteAddr()
}
func (sess *session) LocalAddr() net.Addr {
return sess.conn.LocalAddr()
}
func (sess *session) Environ() []string {
return append([]string(nil), sess.env...)
}
func (sess *session) Command() []string {
return append([]string(nil), sess.cmd...)
}
func (sess *session) Pty() (Pty, <-chan Window, bool) {
if sess.pty != nil {
return *sess.pty, sess.winch, true
}
return Pty{}, sess.winch, false
}
func (sess *session) Signals(c chan<- Signal) {
sess.Lock()
defer sess.Unlock()
sess.sigCh = c
if len(sess.sigBuf) > 0 {
go func() {
for _, sig := range sess.sigBuf {
sess.sigCh <- sig
}
}()
}
}
func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
for req := range reqs {
switch req.Type {
case "shell", "exec":
if sess.handled {
req.Reply(false, nil)
continue
}
sess.handled = true
req.Reply(true, nil)
var payload = struct{ Value string }{}
gossh.Unmarshal(req.Payload, &payload)
sess.cmd, _ = shlex.Split(payload.Value, true)
go func() {
sess.handler(sess)
sess.Exit(0)
}()
case "env":
if sess.handled {
req.Reply(false, nil)
continue
}
var kv struct{ Key, Value string }
gossh.Unmarshal(req.Payload, &kv)
sess.env = append(sess.env, fmt.Sprintf("%s=%s", kv.Key, kv.Value))
req.Reply(true, nil)
case "signal":
var payload struct{ Signal string }
gossh.Unmarshal(req.Payload, &payload)
sess.Lock()
if sess.sigCh != nil {
sess.sigCh <- Signal(payload.Signal)
} else {
if len(sess.sigBuf) < maxSigBufSize {
sess.sigBuf = append(sess.sigBuf, Signal(payload.Signal))
}
}
sess.Unlock()
case "pty-req":
if sess.handled || sess.pty != nil {
req.Reply(false, nil)
continue
}
ptyReq, ok := parsePtyRequest(req.Payload)
if !ok {
req.Reply(false, nil)
continue
}
if sess.ptyCb != nil {
ok := sess.ptyCb(sess.ctx, ptyReq)
if !ok {
req.Reply(false, nil)
continue
}
}
sess.pty = &ptyReq
sess.winch = make(chan Window, 1)
sess.winch <- ptyReq.Window
defer func() {
// when reqs is closed
close(sess.winch)
}()
req.Reply(ok, nil)
case "window-change":
if sess.pty == nil {
req.Reply(false, nil)
continue
}
win, ok := parseWinchRequest(req.Payload)
if ok {
sess.pty.Window = win
sess.winch <- win
}
req.Reply(ok, nil)
case agentRequestType:
// TODO: option/callback to allow agent forwarding
SetAgentRequested(sess.ctx)
req.Reply(true, nil)
default:
// TODO: debug log
}
}
}

View File

@@ -1,109 +0,0 @@
package ssh
import (
"crypto/subtle"
"net"
)
type Signal string
// POSIX signals as listed in RFC 4254 Section 6.10.
const (
SIGABRT Signal = "ABRT"
SIGALRM Signal = "ALRM"
SIGFPE Signal = "FPE"
SIGHUP Signal = "HUP"
SIGILL Signal = "ILL"
SIGINT Signal = "INT"
SIGKILL Signal = "KILL"
SIGPIPE Signal = "PIPE"
SIGQUIT Signal = "QUIT"
SIGSEGV Signal = "SEGV"
SIGTERM Signal = "TERM"
SIGUSR1 Signal = "USR1"
SIGUSR2 Signal = "USR2"
)
// DefaultHandler is the default Handler used by Serve.
var DefaultHandler Handler
// Option is a functional option handler for Server.
type Option func(*Server) error
// Handler is a callback for handling established SSH sessions.
type Handler func(Session)
// PublicKeyHandler is a callback for performing public key authentication.
type PublicKeyHandler func(ctx Context, key PublicKey) bool
// PasswordHandler is a callback for performing password authentication.
type PasswordHandler func(ctx Context, password string) bool
// PtyCallback is a hook for allowing PTY sessions.
type PtyCallback func(ctx Context, pty Pty) bool
// ConnCallback is a hook for new connections before handling.
// It allows wrapping for timeouts and limiting by returning
// the net.Conn that will be used as the underlying connection.
type ConnCallback func(conn net.Conn) net.Conn
// LocalPortForwardingCallback is a hook for allowing port forwarding
type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool
// Window represents the size of a PTY window.
type Window struct {
Width int
Height int
}
// Pty represents a PTY request and configuration.
type Pty struct {
Term string
Window Window
// HELP WANTED: terminal modes!
}
// Serve accepts incoming SSH connections on the listener l, creating a new
// connection goroutine for each. The connection goroutines read requests and
// then calls handler to handle sessions. Handler is typically nil, in which
// case the DefaultHandler is used.
func Serve(l net.Listener, handler Handler, options ...Option) error {
srv := &Server{Handler: handler}
for _, option := range options {
if err := srv.SetOption(option); err != nil {
return err
}
}
return srv.Serve(l)
}
// ListenAndServe listens on the TCP network address addr and then calls Serve
// with handler to handle sessions on incoming connections. Handler is typically
// nil, in which case the DefaultHandler is used.
func ListenAndServe(addr string, handler Handler, options ...Option) error {
srv := &Server{Addr: addr, Handler: handler}
for _, option := range options {
if err := srv.SetOption(option); err != nil {
return err
}
}
return srv.ListenAndServe()
}
// Handle registers the handler as the DefaultHandler.
func Handle(handler Handler) {
DefaultHandler = handler
}
// KeysEqual is constant time compare of the keys to avoid timing attacks.
func KeysEqual(ak, bk PublicKey) bool {
//avoid panic if one of the keys is nil, return false instead
if ak == nil || bk == nil {
return false
}
a := ak.Marshal()
b := bk.Marshal()
return (len(a) == len(b) && subtle.ConstantTimeCompare(a, b) == 1)
}

View File

@@ -1,58 +0,0 @@
package ssh
import (
"fmt"
"io"
"net"
gossh "golang.org/x/crypto/ssh"
)
// direct-tcpip data struct as specified in RFC4254, Section 7.2
type forwardData struct {
DestinationHost string
DestinationPort uint32
OriginatorHost string
OriginatorPort uint32
}
func directTcpipHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
d := forwardData{}
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error())
return
}
if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestinationHost, d.DestinationPort) {
newChan.Reject(gossh.Prohibited, "port forwarding is disabled")
return
}
dest := fmt.Sprintf("%s:%d", d.DestinationHost, d.DestinationPort)
var dialer net.Dialer
dconn, err := dialer.DialContext(ctx, "tcp", dest)
if err != nil {
newChan.Reject(gossh.ConnectionFailed, err.Error())
return
}
ch, reqs, err := newChan.Accept()
if err != nil {
dconn.Close()
return
}
go gossh.DiscardRequests(reqs)
go func() {
defer ch.Close()
defer dconn.Close()
io.Copy(ch, dconn)
}()
go func() {
defer ch.Close()
defer dconn.Close()
io.Copy(dconn, ch)
}()
}

View File

@@ -1,89 +0,0 @@
package ssh
import (
"crypto/rand"
"crypto/rsa"
"encoding/binary"
"golang.org/x/crypto/ssh"
)
func generateSigner() (ssh.Signer, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
return ssh.NewSignerFromKey(key)
}
func parsePtyRequest(s []byte) (pty Pty, ok bool) {
term, s, ok := parseString(s)
if !ok {
return
}
width32, s, ok := parseUint32(s)
if width32 < 1 {
ok = false
}
if !ok {
return
}
height32, _, ok := parseUint32(s)
if height32 < 1 {
ok = false
}
if !ok {
return
}
pty = Pty{
Term: term,
Window: Window{
Width: int(width32),
Height: int(height32),
},
}
return
}
func parseWinchRequest(s []byte) (win Window, ok bool) {
width32, s, ok := parseUint32(s)
if width32 < 1 {
ok = false
}
if !ok {
return
}
height32, _, ok := parseUint32(s)
if height32 < 1 {
ok = false
}
if !ok {
return
}
win = Window{
Width: int(width32),
Height: int(height32),
}
return
}
func parseString(in []byte) (out string, rest []byte, ok bool) {
if len(in) < 4 {
return
}
length := binary.BigEndian.Uint32(in)
if uint32(len(in)) < 4+length {
return
}
out = string(in[4 : 4+length])
rest = in[4+length:]
ok = true
return
}
func parseUint32(in []byte) (uint32, []byte, bool) {
if len(in) < 4 {
return 0, nil, false
}
return binary.BigEndian.Uint32(in), in[4:], true
}

View File

@@ -1,33 +0,0 @@
package ssh
import gossh "golang.org/x/crypto/ssh"
// PublicKey is an abstraction of different types of public keys.
type PublicKey interface {
gossh.PublicKey
}
// The Permissions type holds fine-grained permissions that are specific to a
// user or a specific authentication method for a user. Permissions, except for
// "source-address", must be enforced in the server application layer, after
// successful authentication.
type Permissions struct {
*gossh.Permissions
}
// A Signer can create signatures that verify against a public key.
type Signer interface {
gossh.Signer
}
// ParseAuthorizedKey parses a public key from an authorized_keys file used in
// OpenSSH according to the sshd(8) manual page.
func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) {
return gossh.ParseAuthorizedKey(in)
}
// ParsePublicKey parses an SSH public key formatted for use in
// the SSH wire protocol according to RFC 4253, section 6.6.
func ParsePublicKey(in []byte) (out PublicKey, err error) {
return gossh.ParsePublicKey(in)
}

View File

@@ -1,8 +0,0 @@
MIT License
Copyright (c) 2016 Andrey Nering
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.

View File

@@ -1,170 +0,0 @@
[![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)](https://github.com/go-gormigrate/gormigrate/blob/master/LICENSE)
[![GoDoc](https://godoc.org/gopkg.in/gormigrate.v1?status.svg)](https://godoc.org/gopkg.in/gormigrate.v1)
[![Go Report Card](https://goreportcard.com/badge/gopkg.in/gormigrate.v1)](https://goreportcard.com/report/gopkg.in/gormigrate.v1)
[![Build Status](https://travis-ci.org/go-gormigrate/gormigrate.svg?branch=master)](https://travis-ci.org/go-gormigrate/gormigrate)
# Gormigrate
[![Join the chat at https://gitter.im/go-gormigrate/gormigrate](https://badges.gitter.im/go-gormigrate/gormigrate.svg)](https://gitter.im/go-gormigrate/gormigrate?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Gormigrate is a migration helper for [Gorm][gorm].
Gorm already have useful [migrate functions][gormmigrate], just misses
proper schema versioning and rollback cababilities.
## Supported databases
It supports the databases [Gorm supports][gormdatabases]:
- PostgreSQL
- MySQL
- SQLite
- Microsoft SQL Server
## Installing
```bash
go get -u gopkg.in/gormigrate.v1
```
## Usage
```go
package main
import (
"log"
"gopkg.in/gormigrate.v1"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
func main() {
db, err := gorm.Open("sqlite3", "mydb.sqlite3")
if err != nil {
log.Fatal(err)
}
if err = db.DB().Ping(); err != nil {
log.Fatal(err)
}
db.LogMode(true)
m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
// create persons table
{
ID: "201608301400",
Migrate: func(tx *gorm.DB) error {
// it's a good pratice to copy the struct inside the function,
// so side effects are prevented if the original struct changes during the time
type Person struct {
gorm.Model
Name string
}
return tx.AutoMigrate(&Person{}).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.DropTable("people").Error
},
},
// add age column to persons
{
ID: "201608301415",
Migrate: func(tx *gorm.DB) error {
// when table already exists, it just adds fields as columns
type Person struct {
Age int
}
return tx.AutoMigrate(&Person{}).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Table("people").DropColumn("age").Error
},
},
// add pets table
{
ID: "201608301430",
Migrate: func(tx *gorm.DB) error {
type Pet struct {
gorm.Model
Name string
PersonID int
}
return tx.AutoMigrate(&Pet{}).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.DropTable("pets").Error
},
},
})
if err = m.Migrate(); err != nil {
log.Fatalf("Could not migrate: %v", err)
}
log.Printf("Migration did run successfully")
}
```
## Having a separated function for initializing the schema
If you have a lot of migrations, it can be a pain to run all them, as example,
when you are deploying a new instance of the app, in a clean database.
To prevent this, you can set a function that will run if no migration was run
before (in a new clean database). Remember to create everything here, all tables,
foreign keys and what more you need in your app.
```go
type Person struct {
gorm.Model
Name string
Age int
}
type Pet struct {
gorm.Model
Name string
PersonID int
}
m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
// you migrations here
})
m.InitSchema(func(tx *gorm.DB) error {
err := tx.AutoMigrate(
&Person{},
&Pet{},
// all other tables of you app
)
if err != nil {
return err
}
if err := tx.Model(Pet{}).AddForeignKey("person_id", "people (id)", "RESTRICT", "RESTRICT").Error; err != nil {
return err
}
// all other foreign keys...
return nil
})
```
## Options
This is the options struct, in case you don't want the defaults:
```go
type Options struct {
// Migrations table name. Default to "migrations".
TableName string
// The of the column that stores the id of migrations. Defaults to "id".
IDColumnName string
// UseTransaction makes Gormigrate execute migrations inside a single transaction.
// Keep in mind that not all databases support DDL commands inside transactions.
// Defaults to false.
UseTransaction bool
}
```
[gorm]: http://jinzhu.me/gorm/
[gormmigrate]: http://jinzhu.me/gorm/database.html#migration
[gormdatabases]: http://jinzhu.me/gorm/database.html#connecting-to-a-database

View File

@@ -1,16 +0,0 @@
# https://github.com/go-task/task
dl-deps:
desc: Downloads cli dependencies
cmds:
- go get -u github.com/golang/lint/golint
lint:
desc: Runs golint on this project
cmds:
- golint .
test:
desc: Runs automated tests for this project
cmds:
- go test -v .

View File

@@ -1,68 +0,0 @@
// Package gormigrate is a migration helper for Gorm (http://jinzhu.me/gorm/).
// Gorm already have useful migrate functions
// (http://jinzhu.me/gorm/database.html#migration), just misses
// proper schema versioning and rollback cababilities.
//
// Example:
//
// package main
//
// import (
// "log"
//
// "github.com/go-gormigrate/gormigrate"
// "github.com/jinzhu/gorm"
// _ "github.com/jinzhu/gorm/dialects/sqlite"
// )
//
// type Person struct {
// gorm.Model
// Name string
// }
//
// type Pet struct {
// gorm.Model
// Name string
// PersonID int
// }
//
// func main() {
// db, err := gorm.Open("sqlite3", "mydb.sqlite3")
// if err != nil {
// log.Fatal(err)
// }
// if err = db.DB().Ping(); err != nil {
// log.Fatal(err)
// }
//
// db.LogMode(true)
//
// m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
// {
// ID: "201608301400",
// Migrate: func(tx *gorm.DB) error {
// return tx.AutoMigrate(&Person{}).Error
// },
// Rollback: func(tx *gorm.DB) error {
// return tx.DropTable("people").Error
// },
// },
// {
// ID: "201608301430",
// Migrate: func(tx *gorm.DB) error {
// return tx.AutoMigrate(&Pet{}).Error
// },
// Rollback: func(tx *gorm.DB) error {
// return tx.DropTable("pets").Error
// },
// },
// })
//
// err = m.Migrate()
// if err == nil {
// log.Printf("Migration did run successfully")
// } else {
// log.Printf("Could not migrate: %v", err)
// }
// }
package gormigrate

View File

@@ -1,285 +0,0 @@
package gormigrate
import (
"errors"
"fmt"
"github.com/jinzhu/gorm"
)
// MigrateFunc is the func signature for migrating.
type MigrateFunc func(*gorm.DB) error
// RollbackFunc is the func signature for rollbacking.
type RollbackFunc func(*gorm.DB) error
// InitSchemaFunc is the func signature for initializing the schema.
type InitSchemaFunc func(*gorm.DB) error
// Options define options for all migrations.
type Options struct {
// TableName is the migration table.
TableName string
// IDColumnName is the name of column where the migration id will be stored.
IDColumnName string
// IDColumnSize is the length of the migration id column
IDColumnSize int
// UseTransaction makes Gormigrate execute migrations inside a single transaction.
// Keep in mind that not all databases support DDL commands inside transactions.
UseTransaction bool
}
// Migration represents a database migration (a modification to be made on the database).
type Migration struct {
// ID is the migration identifier. Usually a timestamp like "201601021504".
ID string
// Migrate is a function that will br executed while running this migration.
Migrate MigrateFunc
// Rollback will be executed on rollback. Can be nil.
Rollback RollbackFunc
}
// Gormigrate represents a collection of all migrations of a database schema.
type Gormigrate struct {
db *gorm.DB
tx *gorm.DB
options *Options
migrations []*Migration
initSchema InitSchemaFunc
}
// DuplicatedIDError is returned when more than one migration have the same ID
type DuplicatedIDError struct {
ID string
}
func (e *DuplicatedIDError) Error() string {
return fmt.Sprintf(`Duplicated migration ID: "%s"`, e.ID)
}
var (
// DefaultOptions can be used if you don't want to think about options.
DefaultOptions = &Options{
TableName: "migrations",
IDColumnName: "id",
IDColumnSize: 255,
UseTransaction: false,
}
// ErrRollbackImpossible is returned when trying to rollback a migration
// that has no rollback function.
ErrRollbackImpossible = errors.New("It's impossible to rollback this migration")
// ErrNoMigrationDefined is returned when no migration is defined.
ErrNoMigrationDefined = errors.New("No migration defined")
// ErrMissingID is returned when the ID od migration is equal to ""
ErrMissingID = errors.New("Missing ID in migration")
// ErrNoRunnedMigration is returned when any runned migration was found while
// running RollbackLast
ErrNoRunnedMigration = errors.New("Could not find last runned migration")
)
// New returns a new Gormigrate.
func New(db *gorm.DB, options *Options, migrations []*Migration) *Gormigrate {
if options.TableName == "" {
options.TableName = DefaultOptions.TableName
}
if options.IDColumnName == "" {
options.IDColumnName = DefaultOptions.IDColumnName
}
if options.IDColumnSize == 0 {
options.IDColumnSize = DefaultOptions.IDColumnSize
}
return &Gormigrate{
db: db,
options: options,
migrations: migrations,
}
}
// InitSchema sets a function that is run if no migration is found.
// The idea is preventing to run all migrations when a new clean database
// is being migrating. In this function you should create all tables and
// foreign key necessary to your application.
func (g *Gormigrate) InitSchema(initSchema InitSchemaFunc) {
g.initSchema = initSchema
}
// Migrate executes all migrations that did not run yet.
func (g *Gormigrate) Migrate() error {
if err := g.checkDuplicatedID(); err != nil {
return err
}
if err := g.createMigrationTableIfNotExists(); err != nil {
return err
}
g.begin()
if g.initSchema != nil && g.isFirstRun() {
if err := g.runInitSchema(); err != nil {
g.rollback()
return err
}
return g.commit()
}
for _, migration := range g.migrations {
if err := g.runMigration(migration); err != nil {
g.rollback()
return err
}
}
return g.commit()
}
func (g *Gormigrate) checkDuplicatedID() error {
lookup := make(map[string]struct{}, len(g.migrations))
for _, m := range g.migrations {
if _, ok := lookup[m.ID]; ok {
return &DuplicatedIDError{ID: m.ID}
}
lookup[m.ID] = struct{}{}
}
return nil
}
// RollbackLast undo the last migration
func (g *Gormigrate) RollbackLast() error {
if len(g.migrations) == 0 {
return ErrNoMigrationDefined
}
lastRunnedMigration, err := g.getLastRunnedMigration()
if err != nil {
return err
}
if err := g.RollbackMigration(lastRunnedMigration); err != nil {
return err
}
return nil
}
func (g *Gormigrate) getLastRunnedMigration() (*Migration, error) {
for i := len(g.migrations) - 1; i >= 0; i-- {
migration := g.migrations[i]
if g.migrationDidRun(migration) {
return migration, nil
}
}
return nil, ErrNoRunnedMigration
}
// RollbackMigration undo a migration.
func (g *Gormigrate) RollbackMigration(m *Migration) error {
if m.Rollback == nil {
return ErrRollbackImpossible
}
g.begin()
if err := m.Rollback(g.tx); err != nil {
return err
}
sql := fmt.Sprintf("DELETE FROM %s WHERE %s = ?", g.options.TableName, g.options.IDColumnName)
if err := g.db.Exec(sql, m.ID).Error; err != nil {
g.rollback()
return err
}
return g.commit()
}
func (g *Gormigrate) runInitSchema() error {
if err := g.initSchema(g.tx); err != nil {
return err
}
for _, migration := range g.migrations {
if err := g.insertMigration(migration.ID); err != nil {
return err
}
}
return nil
}
func (g *Gormigrate) runMigration(migration *Migration) error {
if len(migration.ID) == 0 {
return ErrMissingID
}
if !g.migrationDidRun(migration) {
if err := migration.Migrate(g.tx); err != nil {
return err
}
if err := g.insertMigration(migration.ID); err != nil {
return err
}
}
return nil
}
func (g *Gormigrate) createMigrationTableIfNotExists() error {
if g.db.HasTable(g.options.TableName) {
return nil
}
sql := fmt.Sprintf("CREATE TABLE %s (%s VARCHAR(%d) PRIMARY KEY)", g.options.TableName, g.options.IDColumnName, g.options.IDColumnSize)
if err := g.db.Exec(sql).Error; err != nil {
return err
}
return nil
}
func (g *Gormigrate) migrationDidRun(m *Migration) bool {
var count int
g.db.
Table(g.options.TableName).
Where(fmt.Sprintf("%s = ?", g.options.IDColumnName), m.ID).
Count(&count)
return count > 0
}
func (g *Gormigrate) isFirstRun() bool {
var count int
g.db.
Table(g.options.TableName).
Count(&count)
return count == 0
}
func (g *Gormigrate) insertMigration(id string) error {
sql := fmt.Sprintf("INSERT INTO %s (%s) VALUES (?)", g.options.TableName, g.options.IDColumnName)
return g.tx.Exec(sql, id).Error
}
func (g *Gormigrate) begin() {
if g.options.UseTransaction {
g.tx = g.db.Begin()
} else {
g.tx = g.db
}
}
func (g *Gormigrate) commit() error {
if g.options.UseTransaction {
if err := g.tx.Commit().Error; err != nil {
return err
}
}
return nil
}
func (g *Gormigrate) rollback() {
if g.options.UseTransaction {
g.tx.Rollback()
}
}

View File

@@ -1,75 +0,0 @@
# This is the official list of Go-MySQL-Driver authors for copyright purposes.
# If you are submitting a patch, please add your name or the name of the
# organization which holds the copyright to this list in alphabetical order.
# Names should be added to this file as
# Name <email address>
# The email address is not required for organizations.
# Please keep the list sorted.
# Individual Persons
Aaron Hopkins <go-sql-driver at die.net>
Achille Roussel <achille.roussel at gmail.com>
Arne Hormann <arnehormann at gmail.com>
Asta Xie <xiemengjun at gmail.com>
Bulat Gaifullin <gaifullinbf at gmail.com>
Carlos Nieto <jose.carlos at menteslibres.net>
Chris Moos <chris at tech9computers.com>
Daniel Nichter <nil at codenode.com>
Daniël van Eeden <git at myname.nl>
Dave Protasowski <dprotaso at gmail.com>
DisposaBoy <disposaboy at dby.me>
Egor Smolyakov <egorsmkv at gmail.com>
Evan Shaw <evan at vendhq.com>
Frederick Mayle <frederickmayle at gmail.com>
Gustavo Kristic <gkristic at gmail.com>
Hanno Braun <mail at hannobraun.com>
Henri Yandell <flamefew at gmail.com>
Hirotaka Yamamoto <ymmt2005 at gmail.com>
ICHINOSE Shogo <shogo82148 at gmail.com>
INADA Naoki <songofacandy at gmail.com>
Jacek Szwec <szwec.jacek at gmail.com>
James Harr <james.harr at gmail.com>
Jeff Hodges <jeff at somethingsimilar.com>
Jeffrey Charles <jeffreycharles at gmail.com>
Jian Zhen <zhenjl at gmail.com>
Joshua Prunier <joshua.prunier at gmail.com>
Julien Lefevre <julien.lefevr at gmail.com>
Julien Schmidt <go-sql-driver at julienschmidt.com>
Justin Nuß <nuss.justin at gmail.com>
Kamil Dziedzic <kamil at klecza.pl>
Kevin Malachowski <kevin at chowski.com>
Lennart Rudolph <lrudolph at hmc.edu>
Leonardo YongUk Kim <dalinaum at gmail.com>
Lion Yang <lion at aosc.xyz>
Luca Looz <luca.looz92 at gmail.com>
Lucas Liu <extrafliu at gmail.com>
Luke Scott <luke at webconnex.com>
Maciej Zimnoch <maciej.zimnoch@codilime.com>
Michael Woolnough <michael.woolnough at gmail.com>
Nicola Peduzzi <thenikso at gmail.com>
Olivier Mengué <dolmen at cpan.org>
oscarzhao <oscarzhaosl at gmail.com>
Paul Bonser <misterpib at gmail.com>
Peter Schultz <peter.schultz at classmarkets.com>
Rebecca Chin <rchin at pivotal.io>
Runrioter Wung <runrioter at gmail.com>
Shuode Li <elemount at qq.com>
Soroush Pour <me at soroushjp.com>
Stan Putrya <root.vagner at gmail.com>
Stanley Gunawan <gunawan.stanley at gmail.com>
Xiangyu Hu <xiangyu.hu at outlook.com>
Xiaobing Jiang <s7v7nislands at gmail.com>
Xiuming Chen <cc at cxm.cc>
Zhenye Xie <xiezhenye at gmail.com>
# Organizations
Barracuda Networks, Inc.
Google Inc.
Keybase Inc.
Pivotal Inc.
Stripe Inc.

Some files were not shown because too many files have changed in this diff Show More