Compare commits
164 Commits
guardrails
...
v1.14.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c947ce391 | ||
|
|
44559f0547 | ||
|
|
8234119cd4 | ||
|
|
7a75c13ac4 | ||
|
|
4b10131790 | ||
|
|
a29c6e8338 | ||
|
|
198e0717b5 | ||
|
|
d8fa2f6925 | ||
|
|
16c8c0092e | ||
|
|
b0dfff2d90 | ||
|
|
9d2badf253 | ||
|
|
428344da17 | ||
|
|
0c07ac790a | ||
|
|
365a37959a | ||
|
|
90fd6057cf | ||
|
|
4220f3fb89 | ||
|
|
3e2acfc992 | ||
|
|
9c464b2610 | ||
|
|
5760aece65 | ||
|
|
a24e20252a | ||
|
|
37a7fa1917 | ||
|
|
f1b28b0363 | ||
|
|
e43bb55e70 | ||
|
|
763ced7524 | ||
|
|
54128beb12 | ||
|
|
64ba179cc7 | ||
|
|
bbdb4851a5 | ||
|
|
63719ec00e | ||
|
|
0722497336 | ||
|
|
e74f7221b5 | ||
|
|
f4fc3a90bc | ||
|
|
df3aa6e165 | ||
|
|
986bcd7971 | ||
|
|
7f3ea431a1 | ||
|
|
dae0252857 | ||
|
|
33b8e5272c | ||
|
|
21e73757ac | ||
|
|
bcb5d3b7ef | ||
|
|
d2f3f460b2 | ||
|
|
e06fe6f5a3 | ||
|
|
fb9dabfe6b | ||
|
|
0e0cd8fed5 | ||
|
|
8959e1782f | ||
|
|
33151105e0 | ||
|
|
77b40eb9ed | ||
|
|
075dfd0aa7 | ||
|
|
5cf6b1c218 | ||
|
|
6527746a91 | ||
|
|
020ca9c6b3 | ||
|
|
8c7831480b | ||
|
|
e399dfd8e4 | ||
|
|
be83c7148d | ||
|
|
ce187e8675 | ||
|
|
f13ede4ba7 | ||
|
|
fb061ed419 | ||
|
|
b4a377f269 | ||
|
|
de6f37aa64 | ||
|
|
32219577b8 | ||
|
|
abc7329a71 | ||
|
|
675942e967 | ||
|
|
5b20cd501e | ||
|
|
b6aaf4d7cf | ||
|
|
972e232559 | ||
|
|
851a91b1a0 | ||
|
|
6a068dc430 | ||
|
|
2cdfcf60fe | ||
|
|
5d9e0c367a | ||
|
|
cbf8263033 | ||
|
|
846c73d9bc | ||
|
|
e0b43b1976 | ||
|
|
6a6e788968 | ||
|
|
4754cad42a | ||
|
|
db58e53f3b | ||
|
|
b31acb4348 | ||
|
|
c794c2c076 | ||
|
|
42d6cd44bb | ||
|
|
f9057ca56a | ||
|
|
c2f1999037 | ||
|
|
44b386f7a7 | ||
|
|
89b296db4e | ||
|
|
c16403fb3f | ||
|
|
5e21fb72e6 | ||
|
|
c5681bf880 | ||
|
|
db85d6545d | ||
|
|
9912c3deba | ||
|
|
fc5c342e40 | ||
|
|
60707b3faa | ||
|
|
f36845ac6b | ||
|
|
9f76bd6cad | ||
|
|
c53d5d9964 | ||
|
|
171d461ea5 | ||
|
|
b2b04a1155 | ||
|
|
671ba03b78 | ||
|
|
9095725778 | ||
|
|
8b2e5daba3 | ||
|
|
75b7a5f571 | ||
|
|
4b9e881ad0 | ||
|
|
59f8f52cca | ||
|
|
4adaf83fd3 | ||
|
|
84464a4ea6 | ||
|
|
cafac0b8b5 | ||
|
|
5346300a64 | ||
|
|
1d4554eabc | ||
|
|
50bdba8b70 | ||
|
|
8c785f6dea | ||
|
|
93e6abc9ba | ||
|
|
60d7c85c11 | ||
|
|
883bad2ee5 | ||
|
|
7d68e144f8 | ||
|
|
7f32e38cf8 | ||
|
|
43a96d1636 | ||
|
|
00e7d2e45d | ||
|
|
2e711c3591 | ||
|
|
5d147fc03b | ||
|
|
3dccefbbcb | ||
|
|
7c4995fa4a | ||
|
|
2b8f051414 | ||
|
|
4e17c81d63 | ||
|
|
8b4b677d6a | ||
|
|
47229bf473 | ||
|
|
ec5b567da9 | ||
|
|
03b59fae1c | ||
|
|
ede8b3ecf2 | ||
|
|
7ae90b9199 | ||
|
|
a651da451e | ||
|
|
f220af5c54 | ||
|
|
eebf987900 | ||
|
|
3d5101011f | ||
|
|
2cdc19dfdd | ||
|
|
d8a7b1e16c | ||
|
|
d7490d089c | ||
|
|
774c6c0f64 | ||
|
|
92d11c53de | ||
|
|
c509f65a27 | ||
|
|
20b9e839d3 | ||
|
|
6f4fb24cd0 | ||
|
|
23a89fe1de | ||
|
|
559df1f523 | ||
|
|
ad2b8ebc38 | ||
|
|
3824629d4d | ||
|
|
38224714e1 | ||
|
|
a9f4227bba | ||
|
|
06bde77f51 | ||
|
|
2a2554e7a3 | ||
|
|
5d835011e6 | ||
|
|
0f294cd62d | ||
|
|
8e62d21c25 | ||
|
|
2a5dd63e87 | ||
|
|
06cb424b8f | ||
|
|
7c5864a9c3 | ||
|
|
668e34ccab | ||
|
|
95477715fc | ||
|
|
ecc004a485 | ||
|
|
9f0657374b | ||
|
|
61b7f72e94 | ||
|
|
db000baaa5 | ||
|
|
a1a3a29d00 | ||
|
|
2ea73a941f | ||
|
|
e860b60d20 | ||
|
|
d6be01b9b7 | ||
|
|
64c8e01c33 | ||
|
|
acce797e55 | ||
|
|
175fc8d68b | ||
|
|
b9d1cf69c7 |
BIN
.assets/bastion.jpg
Normal file
|
After Width: | Height: | Size: 249 KiB |
BIN
.assets/cluster-mysql.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 7.1 KiB |
BIN
.assets/demo.gif
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 171 KiB |
@@ -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
|
After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
BIN
.assets/overview.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 34 KiB |
@@ -1,21 +1,21 @@
|
||||
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.7
|
||||
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:
|
||||
go.build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- run: make install
|
||||
- run: go get -v -t .
|
||||
- run: make test
|
||||
# - run: make integration
|
||||
- run: go get -u github.com/alecthomas/gometalinter
|
||||
- run: gometalinter --install
|
||||
- run: make lint
|
||||
docker.integration:
|
||||
<<: *defaults
|
||||
steps:
|
||||
@@ -24,17 +24,16 @@ 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:
|
||||
version: 2
|
||||
build_and_integration:
|
||||
jobs:
|
||||
- go.build
|
||||
- docker.integration
|
||||
# requires: docker.build?
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# .git/ # should be kept for git-based versionning
|
||||
|
||||
examples/
|
||||
.circleci/
|
||||
.assets/
|
||||
|
||||
17
.gitattributes
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# Collapse vendored and generated files on GitHub
|
||||
AUTHORS linguist-generated
|
||||
vendor/* linguist-vendored
|
||||
rules.mk linguist-vendored
|
||||
*/vendor/* linguist-vendored
|
||||
*.gen.* linguist-generated
|
||||
*.pb.go linguist-generated
|
||||
*.pb.gw.go linguist-generated
|
||||
go.sum linguist-generated
|
||||
go.mod linguist-generated
|
||||
gen.sum linguist-generated
|
||||
|
||||
# Reduce conflicts on markdown files
|
||||
*.md merge=union
|
||||
6
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
github: ["moul"]
|
||||
patreon: moul
|
||||
open_collective: sshportal
|
||||
custom:
|
||||
- "https://www.buymeacoffee.com/moul"
|
||||
- "https://manfred.life/donate"
|
||||
7
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"groupName": "all",
|
||||
"gomodTidy": true
|
||||
}
|
||||
88
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the Docker image
|
||||
run: docker build . --file Dockerfile
|
||||
golangci-lint:
|
||||
name: golangci-lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: lint
|
||||
uses: golangci/golangci-lint-action@v0.1.7
|
||||
with:
|
||||
version: v1.28
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tests-on-windows:
|
||||
needs: golangci-lint # run after golangci-lint action to not produce duplicated errors
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
golang:
|
||||
#- 1.13
|
||||
- 1.14
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.golang }}
|
||||
- name: Run tests on Windows
|
||||
run: make.exe unittest
|
||||
continue-on-error: true
|
||||
tests-on-mac:
|
||||
needs: golangci-lint # run after golangci-lint action to not produce duplicated errors
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
golang:
|
||||
- 1.14
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.golang }}
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ matrix.golang }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ matrix.golang }}-
|
||||
- name: Run tests on Unix-like operating systems
|
||||
run: make unittest
|
||||
tests-on-linux:
|
||||
needs: golangci-lint # run after golangci-lint action to not produce duplicated errors
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
golang:
|
||||
- 1.11
|
||||
- 1.12
|
||||
- 1.13
|
||||
- 1.14
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.golang }}
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ matrix.golang }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ matrix.golang }}-
|
||||
- name: Run tests on Unix-like operating systems
|
||||
run: make unittest
|
||||
13
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Semantic Release
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
semantic-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: codfish/semantic-release-action@v1
|
||||
if: github.ref == 'refs/heads/master'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
10
.gitignore
vendored
@@ -1,4 +1,12 @@
|
||||
coverage.txt
|
||||
dist/
|
||||
*~
|
||||
*#
|
||||
.*#
|
||||
.DS_Store
|
||||
/log/
|
||||
/sshportal
|
||||
*.db
|
||||
/data
|
||||
/data
|
||||
sshportal.history
|
||||
.idea
|
||||
|
||||
55
.golangci.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
run:
|
||||
deadline: 1m
|
||||
tests: false
|
||||
skip-files:
|
||||
- "testing.go"
|
||||
- ".*\\.pb\\.go"
|
||||
- ".*\\.gen\\.go"
|
||||
|
||||
linters-settings:
|
||||
golint:
|
||||
min-confidence: 0
|
||||
maligned:
|
||||
suggest-new: true
|
||||
goconst:
|
||||
min-len: 5
|
||||
min-occurrences: 4
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- depguard
|
||||
- dogsled
|
||||
#- dupl
|
||||
- errcheck
|
||||
#- funlen
|
||||
- gochecknoinits
|
||||
#- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
#- gocyclo
|
||||
- gofmt
|
||||
- goimports
|
||||
- golint
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
#- maligned
|
||||
- misspell
|
||||
- nakedret
|
||||
- prealloc
|
||||
- scopelint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
#- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
||||
29
.goreleaser.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
builds:
|
||||
-
|
||||
goos: [linux, darwin]
|
||||
goarch: [386, amd64, arm, arm64]
|
||||
ldflags:
|
||||
- -s -w -X main.GitSha={{.ShortCommit}} -X main.GitBranch=master -X main.GitTag={{.Version}}
|
||||
archives:
|
||||
- wrap_in_directory: true
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
brews:
|
||||
-
|
||||
name: sshportal
|
||||
github:
|
||||
owner: moul
|
||||
name: homebrew-moul
|
||||
commit_author:
|
||||
name: moul-bot
|
||||
email: "m+bot@42.am"
|
||||
homepage: https://manfred.life/sshportal
|
||||
description: "Simple, fun and transparent SSH (and telnet) bastion"
|
||||
8
.releaserc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
branch: 'master',
|
||||
plugins: [
|
||||
'@semantic-release/commit-analyzer',
|
||||
'@semantic-release/release-notes-generator',
|
||||
'@semantic-release/github',
|
||||
],
|
||||
};
|
||||
31
AUTHORS
generated
Normal file
@@ -0,0 +1,31 @@
|
||||
# This file lists all individuals having contributed content to the repository.
|
||||
# For how it is generated, see 'https://github.com/moul/rules.mk'
|
||||
|
||||
ahh <ahamidullah@gmail.com>
|
||||
Alen Masic <alenn.masic@gmail.com>
|
||||
Alexander Turner <me@alexturner.co>
|
||||
bozzo <bozzo@users.noreply.github.com>
|
||||
fossabot <badges@fossa.io>
|
||||
Jean-Louis Férey <jeanlouis.ferey@orange.com>
|
||||
jerard@alfa-safety.fr <jrrdev@users.noreply.github.com>
|
||||
Jess <jessachandler@gmail.com>
|
||||
Jonathan Lestrelin <jonathan.lestrelin@gmail.com>
|
||||
Julien Dessaux <julien.dessaux@adyxax.org>
|
||||
Manfred Touron <94029+moul@users.noreply.github.com>
|
||||
Manfred Touron <m@42.am>
|
||||
Manuel <manuel.sabban@nbs-system.com>
|
||||
Manuel Sabban <manu@sabban.eu>
|
||||
Manuel Sabban <msa@nbs-system.com>
|
||||
Mathieu Pasquet <mathieu.pasquet@alterway.fr>
|
||||
Mikael Rapp <micke.rapp@gmail.com>
|
||||
MitaliBo <mitali.bisht14@gmail.com>
|
||||
moul-bot <bot@moul.io>
|
||||
Nelly Asher <karmelylle@rambler.ru>
|
||||
NocFlame <aad@nocflame.se>
|
||||
Quentin Perez <qperez42@gmail.com>
|
||||
Renovate Bot <bot@renovateapp.com>
|
||||
Shawn Wang <shawn111@gmail.com>
|
||||
Valentin Daviot <valentin.daviot@alterway.fr>
|
||||
valentin.daviot <valentin.daviot@alterway.fr>
|
||||
welderpb <welderpb@users.noreply.github.com>
|
||||
Дмитрий Шульгачик <tech@uniplug.ru>
|
||||
93
CHANGELOG.md
@@ -1,94 +1,3 @@
|
||||
# Changelog
|
||||
|
||||
## master (unreleased)
|
||||
|
||||
* No entry
|
||||
|
||||
## v1.8.0 (2018-04-02)
|
||||
|
||||
* The default created user now has the same username as the user starting sshportal (was hardcoded "admin")
|
||||
* Add Telnet support
|
||||
* Add TTY audit feature ([#23](https://github.com/moul/sshportal/issues/23)) by [@sabban](https://github.com/sabban)
|
||||
* Fix `--assign-*` commands when using MySQL driver ([#45](https://github.com/moul/sshportal/issues/45))
|
||||
* Add *HOP* support, an efficient and integrated way of using a jump host transparently ([#47](https://github.com/moul/sshportal/issues/47)) by [@mathieui](https://github.com/mathieui)
|
||||
* Fix panic on some `ls` commands ([#54](https://github.com/moul/sshportal/pull/54)) by [@jle64](https://github.com/jle64)
|
||||
* Add tunnels (`direct-tcp`) support with logging ([#44](https://github.com/moul/sshportal/issues/44)) by [@sabban](https://github.com/sabban)
|
||||
* Add `key import` command ([#52](https://github.com/moul/sshportal/issues/52)) by [@adyxax](https://github.com/adyxax)
|
||||
* Add 'exec' logging ([#40](https://github.com/moul/sshportal/issues/40)) by [@sabban](https://github.com/sabban)
|
||||
|
||||
## v1.7.1 (2018-01-03)
|
||||
|
||||
* Return non-null exit-code on authentication error
|
||||
* **hotfix**: repair invite system (broken in v1.7.0)
|
||||
|
||||
## v1.7.0 (2018-01-02)
|
||||
|
||||
Breaking changes:
|
||||
* Use `sshportal server` instead of `sshportal` to start a new server (nothing to change if using the docker image)
|
||||
* Remove `--config-user` and `--healthcheck-user` global options
|
||||
|
||||
Changes:
|
||||
* Fix connection failure when sending too many environment variables (fix [#22](https://github.com/moul/sshportal/issues/22))
|
||||
* Fix panic when entering empty command (fix [#13](https://github.com/moul/sshportal/issues/13))
|
||||
* Add `config backup --ignore-events` option
|
||||
* Add `sshportal healthcheck [--addr=] [--wait] [--quiet]` cli command
|
||||
* Add [Docker Healthcheck](https://docs.docker.com/engine/reference/builder/#healthcheck) helper
|
||||
* Support Putty (fix [#24](https://github.com/moul/sshportal/issues/24))
|
||||
|
||||
## v1.6.0 (2017-12-12)
|
||||
|
||||
* Add `--latest` and `--quiet` options to `ls` commands
|
||||
* Add `healthcheck` user
|
||||
* Add `key show KEY` command
|
||||
|
||||
## v1.5.0 (2017-12-02)
|
||||
|
||||
* Create Session objects on each connections (history)
|
||||
* Connection history
|
||||
* Audit log
|
||||
* Add dynamic strict host key checking (learning on the first time, strict on the next ones)
|
||||
* Add-back MySQL support (experimental)
|
||||
* Fix some backup/restore bugs
|
||||
|
||||
## v1.4.0 (2017-11-24)
|
||||
|
||||
* Add 'key setup' command (easy SSH key installation)
|
||||
* Add Updated and Created fields in 'ls' commands
|
||||
* Add `--aes-key` option to encrypt sensitive data
|
||||
|
||||
## v1.3.0 (2017-11-23)
|
||||
|
||||
* More details in 'ls' commands
|
||||
* Add 'host update' command (fix [#2](https://github.com/moul/sshportal/issues/2))
|
||||
* Add 'user update' command (fix [#3](https://github.com/moul/sshportal/issues/3))
|
||||
* Add 'acl update' command (fix [#4](https://github.com/moul/sshportal/issues/4))
|
||||
* Allow connecting to the shell mode with the registered username or email (fix [#5](https://github.com/moul/sshportal/issues/5))
|
||||
* Add 'listhosts' role (fix [#5](https://github.com/moul/sshportal/issues/5))
|
||||
|
||||
## v1.2.0 (2017-11-22)
|
||||
|
||||
* Support adding multiple `--group` links on `host create` and `user create`
|
||||
* Use govalidator to perform more consistent input validation
|
||||
* Use a database migration system
|
||||
|
||||
## v1.1.0 (2017-11-15)
|
||||
|
||||
* Improve versionning (static VERSION + dynamic GIT_* info)
|
||||
* Configuration management (backup + restore)
|
||||
* Implement Exit (fix [#6](https://github.com/moul/sshportal/pull/6))
|
||||
* Disable mysql support (not fully working right now)
|
||||
* Set random seed properly
|
||||
|
||||
## v1.0.0 (2017-11-14)
|
||||
|
||||
Initial version
|
||||
|
||||
* Host management
|
||||
* User management
|
||||
* User Group management
|
||||
* Host Group management
|
||||
* Host Key management
|
||||
* User Key management
|
||||
* ACL management
|
||||
* Connect to host using key or password
|
||||
* Admin commands can be run directly or in an interactive shell
|
||||
Here: https://github.com/moul/sshportal/releases
|
||||
|
||||
@@ -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.7 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
|
||||
|
||||
51
Makefile
@@ -1,46 +1,47 @@
|
||||
GIT_SHA ?= $(shell git rev-parse HEAD)
|
||||
GIT_TAG ?= $(shell git describe --tags --always)
|
||||
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
LDFLAGS ?= -X main.GitSha=$(GIT_SHA) -X main.GitTag=$(GIT_TAG) -X main.GitBranch=$(GIT_BRANCH)
|
||||
VERSION ?= $(shell grep 'VERSION =' main.go | cut -d'"' -f2)
|
||||
GOPKG ?= moul.io/sshportal
|
||||
GOBINS ?= .
|
||||
DOCKER_IMAGE ?= moul/sshportal
|
||||
|
||||
VERSION ?= `git describe --tags --always`
|
||||
VCS_REF ?= `git rev-parse --short HEAD`
|
||||
GO_INSTALL_OPTS = -ldflags="-X main.GitSha=$(VCS_REF) -X main.GitTag=$(VERSION)"
|
||||
PORT ?= 2222
|
||||
|
||||
include rules.mk
|
||||
|
||||
DB_VERSION ?= v$(shell grep -E 'ID: "[0-9]+",' pkg/bastion/dbinit.go | tail -n 1 | cut -d'"' -f2)
|
||||
AES_KEY ?= my-dummy-aes-key
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
go install -v -ldflags '$(LDFLAGS)' .
|
||||
|
||||
.PHONY: docker.build
|
||||
docker.build:
|
||||
docker build -t moul/sshportal .
|
||||
|
||||
.PHONY: integration
|
||||
integration:
|
||||
cd ./examples/integration && make
|
||||
|
||||
.PHONY: _docker_install
|
||||
_docker_install:
|
||||
CGO_ENABLED=1 go build -ldflags '-extldflags "-static" $(LDFLAGS)' -tags netgo -v -o /go/bin/sshportal
|
||||
CGO_ENABLED=1 $(GO) build -ldflags '-extldflags "-static" $(LDFLAGS)' -tags netgo -v -o /go/bin/sshportal
|
||||
|
||||
.PHONY: dev
|
||||
dev:
|
||||
-go get github.com/githubnemo/CompileDaemon
|
||||
-$(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 .
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
gometalinter --disable-all --enable=errcheck --enable=vet --enable=vetshadow --enable=golint --enable=gas --enable=ineffassign --enable=goconst --enable=goimports --enable=gofmt --exclude="should have comment" --enable=staticcheck --enable=gosimple --enable=misspell --deadline=60s .
|
||||
|
||||
.PHONY: backup
|
||||
backup:
|
||||
mkdir -p data/backups
|
||||
cp sshportal.db data/backups/$(shell date +%s)-$(VERSION)-sshportal.sqlite
|
||||
cp sshportal.db data/backups/$(shell date +%s)-$(DB_VERSION)-sshportal.sqlite
|
||||
|
||||
doc:
|
||||
dot -Tsvg ./.assets/overview.dot > ./.assets/overview.svg
|
||||
dot -Tsvg ./.assets/cluster-mysql.dot > ./.assets/cluster-mysql.svg
|
||||
dot -Tsvg ./.assets/flow-diagram.dot > ./.assets/flow-diagram.svg
|
||||
dot -Tpng ./.assets/overview.dot > ./.assets/overview.png
|
||||
dot -Tpng ./.assets/cluster-mysql.dot > ./.assets/cluster-mysql.png
|
||||
dot -Tpng ./.assets/flow-diagram.dot > ./.assets/flow-diagram.png
|
||||
|
||||
.PHONY: goreleaser
|
||||
goreleaser:
|
||||
GORELEASER_GITHUB_TOKEN=$(GORELEASER_GITHUB_TOKEN) GITHUB_TOKEN=$(GITHUB_TOKEN) goreleaser --rm-dist
|
||||
|
||||
.PHONY: goreleaser-dry-run
|
||||
goreleaser-dry-run:
|
||||
goreleaser --snapshot --skip-publish --rm-dist
|
||||
|
||||
436
README.md
@@ -1,65 +1,45 @@
|
||||
# sshportal
|
||||
|
||||
[](https://circleci.com/gh/moul/sshportal)
|
||||
[](https://hub.docker.com/r/moul/sshportal/)
|
||||
[](https://goreportcard.com/report/github.com/moul/sshportal)
|
||||
[](https://godoc.org/github.com/moul/sshportal)
|
||||
[](https://github.com/moul/sshportal/blob/master/LICENSE)
|
||||
[](https://goreportcard.com/report/moul.io/sshportal)
|
||||
[](https://godoc.org/moul.io/sshportal)
|
||||
[](https://opencollective.com/sshportal) [](https://github.com/moul/sshportal/blob/master/LICENSE)
|
||||
[](https://github.com/moul/sshportal/releases)
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fmoul%2Fsshportal?ref=badge_shield)
|
||||
<!-- temporarily broken? [](https://hub.docker.com/r/moul/sshportal/) -->
|
||||
|
||||
Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
|
||||
|
||||

|
||||
<img src="https://raw.githubusercontent.com/moul/sshportal/master/.assets/bastion.jpg" width="50%">
|
||||
|
||||
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.
|
||||
|
||||

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

|
||||
<!-- 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
|
||||
|
||||
@@ -80,6 +60,8 @@ Shared connection to localhost closed.
|
||||
$
|
||||
```
|
||||
|
||||
If the association fails and you are promted for a password, verify that the host you're connecting from has a SSH key set up or generate one with ```ssh-keygen -t rsa```
|
||||
|
||||
Drop an interactive administrator shell
|
||||
|
||||
```console
|
||||
@@ -139,15 +121,159 @@ To associate this account with a key, use the following SSH user: 'invite:NfHK5a
|
||||
config>
|
||||
```
|
||||
|
||||
## Flow Diagram
|
||||
Demo gif:
|
||||

|
||||
|
||||

|
||||
---
|
||||
|
||||
## 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/)
|
||||
* It is not possible for a user to access a host with the same name as the user. This is easily circumvented by changing the user name, especially since the most common use cases does not expose it.
|
||||
* It is not possible access a host named `healthcheck` as this is a built in command.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
`sshportal` embeds a configuration CLI.
|
||||
|
||||
By default, the configuration user is `admin`, (can be changed using `--config-user=<value>` when starting the server.
|
||||
By default, the configuration user is `admin`, (can be changed using `--config-user=<value>` when starting the server. The shell is also accessible through `ssh [username]@portal.example.org`.
|
||||
|
||||
Each commands can be run directly by using this syntax: `ssh admin@portal.example.org <command> [args]`:
|
||||
|
||||
@@ -157,7 +283,29 @@ ssh admin@portal.example.org host inspect toto
|
||||
|
||||
You can enter in interactive mode using this syntax: `ssh admin@portal.example.org`
|
||||
|
||||
### Synopsis
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
@@ -180,11 +328,11 @@ event inspect [-h] EVENT...
|
||||
|
||||
# host management
|
||||
host help
|
||||
host create [-h] [--name=<value>] [--password=<value>] [--comment=<value>] [--key=KEY] [--group=HOSTGROUP...] [--hop=HOST] <username>[:<password>]@<host>[:<port>]
|
||||
host create [-h] [--name=<value>] [--password=<value>] [--comment=<value>] [--key=KEY] [--group=HOSTGROUP...] [--hop=HOST] [--logging=MODE] <username>[:<password>]@<host>[:<port>]
|
||||
host inspect [-h] [--decrypt] HOST...
|
||||
host ls [-h] [--latest] [--quiet]
|
||||
host rm [-h] HOST...
|
||||
host update [-h] [--name=<value>] [--comment=<value>] [--key=KEY] [--assign-group=HOSTGROUP...] [--unassign-group=HOSTGROUP...] [--set-hop=HOST] [--unset-hop] HOST...
|
||||
host update [-h] [--name=<value>] [--comment=<value>] [--key=KEY] [--assign-group=HOSTGROUP...] [--unassign-group=HOSTGROUP...] [--logging-MODE] [--set-hop=HOST] [--unset-hop] HOST...
|
||||
|
||||
# hostgroup management
|
||||
hostgroup help
|
||||
@@ -230,120 +378,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 +412,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 +447,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`.
|
||||
|
||||

|
||||

|
||||
|
||||
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 +470,38 @@ 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
|
||||
|
||||

|
||||

|
||||
|
||||
## 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
|
||||
[](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>
|
||||
|
||||
### Stargazers over time
|
||||
|
||||
[](https://starchart.cc/moul/sshportal)
|
||||
|
||||
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-slate
|
||||
40
acl.go
@@ -1,40 +0,0 @@
|
||||
package main
|
||||
|
||||
import "sort"
|
||||
|
||||
type ByWeight []*ACL
|
||||
|
||||
func (a ByWeight) Len() int { return len(a) }
|
||||
func (a ByWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByWeight) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
|
||||
|
||||
func CheckACLs(user User, host Host) (string, error) {
|
||||
// shared ACLs between user and host
|
||||
aclMap := map[uint]*ACL{}
|
||||
for _, userGroup := range user.Groups {
|
||||
for _, userGroupACL := range userGroup.ACLs {
|
||||
for _, hostGroup := range host.Groups {
|
||||
for _, hostGroupACL := range hostGroup.ACLs {
|
||||
if userGroupACL.ID == hostGroupACL.ID {
|
||||
aclMap[userGroupACL.ID] = userGroupACL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// FIXME: add ACLs that match host pattern
|
||||
|
||||
// deny by default if no shared ACL
|
||||
if len(aclMap) == 0 {
|
||||
return ACLActionDeny, nil // default action
|
||||
}
|
||||
|
||||
// transform map to slice and sort it
|
||||
acls := make([]*ACL, 0, len(aclMap))
|
||||
for _, acl := range aclMap {
|
||||
acls = append(acls, acl)
|
||||
}
|
||||
sort.Sort(ByWeight(acls))
|
||||
|
||||
return acls[0].Action, nil
|
||||
}
|
||||
47
acl_test.go
@@ -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)
|
||||
})
|
||||
}
|
||||
49
config.go
@@ -1,49 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type configServe struct {
|
||||
aesKey string
|
||||
dbDriver, dbURL string
|
||||
logsLocation string
|
||||
bindAddr string
|
||||
debug, demo bool
|
||||
}
|
||||
|
||||
func parseServeConfig(c *cli.Context) (*configServe, error) {
|
||||
ret := &configServe{
|
||||
aesKey: c.String("aes-key"),
|
||||
dbDriver: c.String("db-driver"),
|
||||
dbURL: c.String("db-conn"),
|
||||
bindAddr: c.String("bind-address"),
|
||||
debug: c.Bool("debug"),
|
||||
demo: c.Bool("demo"),
|
||||
logsLocation: c.String("logs-location"),
|
||||
}
|
||||
switch len(ret.aesKey) {
|
||||
case 0, 16, 24, 32:
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid aes key size, should be 16 or 24, 32")
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func ensureLogDirectory(location string) error {
|
||||
// check for the logdir existence
|
||||
logsLocation, err := os.Stat(location)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return os.MkdirAll(location, os.ModeDir|os.FileMode(0750))
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !logsLocation.IsDir() {
|
||||
return fmt.Errorf("log directory cannot be created")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
34
go.mod
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
module moul.io/sshportal
|
||||
|
||||
require (
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
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.11 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/docker v1.13.1
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/gliderlabs/ssh v0.3.0
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||
github.com/jinzhu/gorm v1.9.15
|
||||
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-20200706080929-d51e80ef957d
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/reiver/go-oi v1.0.0
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
|
||||
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4
|
||||
github.com/urfave/cli v1.22.4
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect
|
||||
gopkg.in/gormigrate.v1 v1.6.0
|
||||
moul.io/srand v1.4.0
|
||||
)
|
||||
|
||||
go 1.14
|
||||
124
go.sum
generated
Normal file
@@ -0,0 +1,124 @@
|
||||
cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/asaskevich/govalidator v0.0.0-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/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
|
||||
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
|
||||
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/gliderlabs/ssh v0.3.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/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
||||
github.com/jinzhu/gorm v1.9.15 h1:OdR1qFvtXktlxk73XFYMiYn9ywzTwytqe4QkuMRqc38=
|
||||
github.com/jinzhu/gorm v1.9.15/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
|
||||
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/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/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
|
||||
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
|
||||
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e h1:quuzZLi72kkJjl+f5AQ93FMcadG19WkS7MO6TXFOSas=
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvEWwEIx86DB9Ke/+a5wBI464eDRo3eF0LcfpWg=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3 h1:yxUGvEatvDMO6gkhwx82Va+Czdyui9LiCw6a5YB/2f8=
|
||||
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3/go.mod h1:1Q04m7wmv/IMoZU9t8UkH+n9McWn4i3H9v9LnMgqloo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 h1:hBSHahWMEgzwRyS6dRpxY0XyjZsHyQ61s084wo5PJe0=
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/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-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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=
|
||||
22
helm/sshportal/.helmignore
Normal file
@@ -0,0 +1,22 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
21
helm/sshportal/Chart.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
apiVersion: v2
|
||||
name: sshportal
|
||||
description: A Helm chart for SSHPortal on Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application.
|
||||
appVersion: 1.10.0
|
||||
33
helm/sshportal/templates/NOTES.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
1. Get the admin invitation token (only on first install):
|
||||
export INVITE=$(kubectl --namespace sshportal logs -l "app.kubernetes.io/name={{ include "sshportal.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" | grep -Eo "invite:[a-zA-Z0-9]+")
|
||||
|
||||
2. Get the service IP and Port:
|
||||
{{- if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sshportal.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sshportal.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sshportal.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sshportal.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 2222:{{ .Values.service.port }}
|
||||
{{- end }}
|
||||
|
||||
3. Enroll your SSH public key:
|
||||
{{- if contains "NodePort" .Values.service.type }}
|
||||
ssh $NODE_IP -p $NODE_PORT -l $INVITE
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
ssh $SERVICE_IP -p {{ .Values.service.port }} -l $INVITE
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
ssh localhost -p 2222 -l $INVITE
|
||||
{{- end }}
|
||||
|
||||
4. Configure your {{ include "sshportal.name" . }} install:
|
||||
{{- if contains "NodePort" .Values.service.type }}
|
||||
ssh admin@$NODE_IP -p $NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
ssh admin@$SERVICE_IP -p {{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
ssh admin@localhost -p 2222
|
||||
{{- end }}
|
||||
63
helm/sshportal/templates/_helpers.tpl
Normal file
@@ -0,0 +1,63 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "sshportal.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "sshportal.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "sshportal.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "sshportal.labels" -}}
|
||||
helm.sh/chart: {{ include "sshportal.chart" . }}
|
||||
{{ include "sshportal.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "sshportal.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "sshportal.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "sshportal.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
{{ default (include "sshportal.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else -}}
|
||||
{{ default "default" .Values.serviceAccount.name }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
69
helm/sshportal/templates/deployment.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "sshportal.fullname" . }}
|
||||
labels:
|
||||
{{- include "sshportal.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "sshportal.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "sshportal.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:v{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: ssh
|
||||
containerPort: 2222
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sshportal
|
||||
- healthcheck
|
||||
- --quiet
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sshportal
|
||||
- healthcheck
|
||||
- --quiet
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.mysql.enabled }}
|
||||
- name: SSHPORTAL_DATABASE_URL
|
||||
value: {{ .Values.mysql.user }}:{{ .Values.mysql.password }}@tcp({{ .Values.mysql.server }}:{{ .Values.mysql.port }})/{{ .Values.mysql.database }}?charset=utf8&parseTime=true&loc=Local
|
||||
- name: SSHPORTAL_DB_DRIVER
|
||||
value: mysql
|
||||
{{- end }}
|
||||
{{- if .Values.debug}}
|
||||
- name: SSHPORTAL_DEBUG
|
||||
value: "1"
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
21
helm/sshportal/templates/horizontal-pod-autoscaling.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
{{- if .Values.mysql.enabled }}
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "sshportal.fullname" . }}
|
||||
labels:
|
||||
{{- include "sshportal.labels" . | nindent 4 }}
|
||||
spec:
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "sshportal.fullname" . }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: {{ .Values.autoscaling.cpuTarget }}
|
||||
{{- end }}
|
||||
|
||||
17
helm/sshportal/templates/service.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "sshportal.fullname" . }}
|
||||
annotations:
|
||||
{{- toYaml .Values.service.annotations | nindent 4 }}
|
||||
labels:
|
||||
{{- include "sshportal.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: 2222
|
||||
protocol: TCP
|
||||
name: ssh
|
||||
selector:
|
||||
{{- include "sshportal.selectorLabels" . | nindent 4 }}
|
||||
15
helm/sshportal/templates/tests/test-connection.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "sshportal.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{ include "sshportal.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test-success
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "sshportal.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
119
helm/sshportal/values.yaml
Normal file
@@ -0,0 +1,119 @@
|
||||
# Default values for sshportal.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
## Enable SSHPortal debug mode
|
||||
##
|
||||
debug: false
|
||||
|
||||
## SSH Portal Docker image
|
||||
##
|
||||
image:
|
||||
repository: moul/sshportal
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
## Reference to one or more secrets to be used when pulling images
|
||||
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
##
|
||||
imagePullSecrets: []
|
||||
|
||||
## Provide a name in place of sshportal for `app:` labels
|
||||
##
|
||||
nameOverride: ""
|
||||
|
||||
## Provide a name to substitute for the full names of resources
|
||||
##
|
||||
fullnameOverride: ""
|
||||
|
||||
## PodSecurityContext holds pod-level security attributes.
|
||||
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
|
||||
##
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
## SecurityContext holds container-level security attributes.
|
||||
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
|
||||
##
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
## Service
|
||||
##
|
||||
service:
|
||||
## Configure additional annotations for SSHPortal service
|
||||
##
|
||||
annotations: {}
|
||||
# service.beta.kubernetes.io/openstack-internal-load-balancer: "true"
|
||||
|
||||
## Service type, one of
|
||||
## NodePort, ClusterIP, LoadBalancer
|
||||
##
|
||||
type: LoadBalancer
|
||||
|
||||
## Port to expose on the service
|
||||
##
|
||||
port: 22
|
||||
|
||||
## Define resources requests and limits
|
||||
## ref: https://kubernetes.io/docs/user-guide/compute-resources/
|
||||
##
|
||||
resources: {}
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# limits:
|
||||
# cpu: 2
|
||||
# memory: 2Gi
|
||||
|
||||
## Mysql/MariaDB configuration for HA
|
||||
##
|
||||
mysql:
|
||||
enabled: false
|
||||
|
||||
## Database user
|
||||
##
|
||||
user: sshportal
|
||||
|
||||
## Database password
|
||||
##
|
||||
password: change_me
|
||||
|
||||
## Database name
|
||||
##
|
||||
database: sshportal
|
||||
|
||||
## Database server FQDN or IP
|
||||
##
|
||||
server: mariadb-mariadb-galera
|
||||
|
||||
## Database port
|
||||
##
|
||||
port: 3306
|
||||
|
||||
## Define which Nodes the Pods are scheduled on.
|
||||
## ref: https://kubernetes.io/docs/user-guide/node-selection/
|
||||
##
|
||||
nodeSelector: {}
|
||||
|
||||
## The pod's tolerations.
|
||||
## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
|
||||
##
|
||||
tolerations: []
|
||||
|
||||
## Assign custom affinity rules
|
||||
## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/
|
||||
##
|
||||
affinity: {}
|
||||
|
||||
## HPA support, require `mysql.enable: true`
|
||||
## This section enables sshportal to autoscale based on metrics.
|
||||
##
|
||||
autoscaling:
|
||||
maxReplicas: 4
|
||||
minReplicas: 2
|
||||
cpuTarget: 60
|
||||
114
main.go
@@ -1,40 +1,34 @@
|
||||
package main
|
||||
package main // import "moul.io/sshportal"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/urfave/cli"
|
||||
"moul.io/srand"
|
||||
)
|
||||
|
||||
var (
|
||||
// Version should be updated by hand at each release
|
||||
Version = "1.8.0+dev"
|
||||
// GitTag will be overwritten automatically by the build system
|
||||
GitTag string
|
||||
GitTag = "n/a"
|
||||
// GitSha will be overwritten automatically by the build system
|
||||
GitSha string
|
||||
// GitBranch will be overwritten automatically by the build system
|
||||
GitBranch string
|
||||
GitSha = "n/a"
|
||||
)
|
||||
|
||||
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.Version = GitTag + " (" + GitSha + ")"
|
||||
app.Email = "https://moul.io/sshportal"
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "server",
|
||||
@@ -43,7 +37,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
|
||||
}
|
||||
@@ -57,27 +51,37 @@ 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",
|
||||
Value: 0,
|
||||
Usage: "Duration before an inactive connection is timed out (0 to disable)",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
@@ -108,55 +112,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,
|
||||
}
|
||||
|
||||
for _, opt := range []ssh.Option{
|
||||
// custom PublicKeyAuth handler
|
||||
ssh.PublicKeyAuth(publicKeyAuthHandler(db, c)),
|
||||
ssh.PasswordAuth(passwordAuthHandler(db, c)),
|
||||
// retrieve sshportal SSH private key from database
|
||||
privateKeyFromDB(db, c.aesKey),
|
||||
} {
|
||||
if err := srv.SetOption(opt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("info: SSH Server accepting connections on %s", c.bindAddr)
|
||||
return srv.Serve(ln)
|
||||
}
|
||||
|
||||
50
pkg/bastion/acl.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package bastion
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
type byWeight []*dbmodels.ACL
|
||||
|
||||
func (a byWeight) Len() int { return len(a) }
|
||||
func (a byWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byWeight) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
|
||||
|
||||
func checkACLs(user dbmodels.User, host dbmodels.Host) string {
|
||||
currentTime := time.Now()
|
||||
|
||||
// shared ACLs between user and host
|
||||
aclMap := map[uint]*dbmodels.ACL{}
|
||||
for _, userGroup := range user.Groups {
|
||||
for _, userGroupACL := range userGroup.ACLs {
|
||||
for _, hostGroup := range host.Groups {
|
||||
for _, hostGroupACL := range hostGroup.ACLs {
|
||||
if userGroupACL.ID == hostGroupACL.ID {
|
||||
if (userGroupACL.Inception == nil || currentTime.After(*userGroupACL.Inception)) &&
|
||||
(userGroupACL.Expiration == nil || currentTime.Before(*userGroupACL.Expiration)) {
|
||||
aclMap[userGroupACL.ID] = userGroupACL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// FIXME: add ACLs that match host pattern
|
||||
|
||||
// deny by default if no shared ACL
|
||||
if len(aclMap) == 0 {
|
||||
return string(dbmodels.ACLActionDeny) // default action
|
||||
}
|
||||
|
||||
// transform map to slice and sort it
|
||||
acls := make([]*dbmodels.ACL, 0, len(aclMap))
|
||||
for _, acl := range aclMap {
|
||||
acls = append(acls, acl)
|
||||
}
|
||||
sort.Sort(byWeight(acls))
|
||||
|
||||
return acls[0].Action
|
||||
}
|
||||
49
pkg/bastion/acl_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
func TestCheckACLs(t *testing.T) {
|
||||
Convey("Testing CheckACLs", t, func(c C) {
|
||||
// create tmp dir
|
||||
tempDir, err := ioutil.TempDir("", "sshportal")
|
||||
c.So(err, ShouldBeNil)
|
||||
defer func() {
|
||||
c.So(os.RemoveAll(tempDir), ShouldBeNil)
|
||||
}()
|
||||
|
||||
// create sqlite db
|
||||
db, err := gorm.Open("sqlite3", filepath.Join(tempDir, "sshportal.db"))
|
||||
c.So(err, ShouldBeNil)
|
||||
db.LogMode(false)
|
||||
c.So(DBInit(db), ShouldBeNil)
|
||||
|
||||
// create dummy objects
|
||||
var hostGroup dbmodels.HostGroup
|
||||
err = dbmodels.HostGroupsByIdentifiers(db, []string{"default"}).First(&hostGroup).Error
|
||||
c.So(err, ShouldBeNil)
|
||||
db.Create(&dbmodels.Host{Groups: []*dbmodels.HostGroup{&hostGroup}})
|
||||
|
||||
//. load db
|
||||
var (
|
||||
hosts []dbmodels.Host
|
||||
users []dbmodels.User
|
||||
)
|
||||
db.Preload("Groups").Preload("Groups.ACLs").Find(&hosts)
|
||||
db.Preload("Groups").Preload("Groups.ACLs").Find(&users)
|
||||
|
||||
// test
|
||||
action := checkACLs(users[0], hosts[0])
|
||||
c.So(action, ShouldEqual, dbmodels.ACLActionAllow)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
@@ -37,15 +40,14 @@ func dbInit(db *gorm.DB) error {
|
||||
ID: "2",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type SSHKey struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string
|
||||
Type string
|
||||
Length uint
|
||||
Fingerprint string
|
||||
PrivKey string `sql:"size:10000"`
|
||||
PubKey string `sql:"size:10000"`
|
||||
Hosts []*Host `gorm:"ForeignKey:SSHKeyID"`
|
||||
PrivKey string `sql:"size:5000"`
|
||||
PubKey string `sql:"size:1000"`
|
||||
Hosts []*dbmodels.Host `gorm:"ForeignKey:SSHKeyID"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&SSHKey{}).Error
|
||||
@@ -57,15 +59,14 @@ func dbInit(db *gorm.DB) error {
|
||||
ID: "3",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32"`
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Fingerprint string
|
||||
Comment string
|
||||
}
|
||||
@@ -79,9 +80,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:1000"`
|
||||
UserID uint ``
|
||||
User *dbmodels.User `gorm:"ForeignKey:UserID"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&UserKey{}).Error
|
||||
@@ -93,13 +94,12 @@ func dbInit(db *gorm.DB) error {
|
||||
ID: "5",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type User struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
IsAdmin bool
|
||||
Email string
|
||||
Name string
|
||||
Keys []*UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string
|
||||
InviteToken string
|
||||
}
|
||||
@@ -114,8 +114,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 +129,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 +143,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 +159,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 +229,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,27 +245,27 @@ 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
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
user.Roles = append(user.Roles, &adminRole)
|
||||
if err := tx.Save(&user).Error; err != nil {
|
||||
if err := tx.Save(user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -279,13 +279,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 +295,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 +324,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 +341,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:1000" valid:"required,length(1|1000)"`
|
||||
AuthorizedKey string `sql:"size:1000" valid:"required,length(1|1000)"`
|
||||
UserID uint ``
|
||||
User *dbmodels.User `gorm:"ForeignKey:UserID"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
return tx.AutoMigrate(&UserKey{}).Error
|
||||
},
|
||||
@@ -355,7 +355,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
|
||||
}
|
||||
@@ -366,7 +366,7 @@ func dbInit(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
userKey.AuthorizedKey = string(gossh.MarshalAuthorizedKey(key))
|
||||
if err := db.Model(&userKey).Updates(&userKey).Error; err != nil {
|
||||
if err := db.Model(userKey).Updates(userKey).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -379,18 +379,17 @@ func dbInit(db *gorm.DB) error {
|
||||
ID: "25",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
|
||||
Addr string `valid:"required"`
|
||||
User string `valid:"optional"`
|
||||
Password string `valid:"optional"`
|
||||
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000" valid:"optional"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Fingerprint string `valid:"optional"` // FIXME: replace with hostKey ?
|
||||
Comment string `valid:"optional"`
|
||||
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
|
||||
Addr string `valid:"required"`
|
||||
User string `valid:"optional"`
|
||||
Password string `valid:"optional"`
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:1000" valid:"optional"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Fingerprint string `valid:"optional"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
},
|
||||
@@ -402,14 +401,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,14 +418,14 @@ 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
|
||||
}
|
||||
|
||||
for _, session := range sessions {
|
||||
if session.StoppedAt != nil && session.StoppedAt.IsZero() {
|
||||
if err := db.Model(&session).Updates(map[string]interface{}{"stopped_at": nil}).Error; err != nil {
|
||||
if err := db.Model(session).Updates(map[string]interface{}{"stopped_at": nil}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -440,17 +439,16 @@ func dbInit(db *gorm.DB) error {
|
||||
ID: "28",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32"`
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
URL string
|
||||
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:1000"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
@@ -462,19 +460,18 @@ func dbInit(db *gorm.DB) error {
|
||||
ID: "29",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32"`
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
URL string
|
||||
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:1000"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string
|
||||
Hop *Host
|
||||
Hop *dbmodels.Host
|
||||
HopID uint
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
@@ -482,12 +479,57 @@ func dbInit(db *gorm.DB) error {
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}, {
|
||||
ID: "30",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Host struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"size:32"`
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
URL string
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string
|
||||
Hop *dbmodels.Host
|
||||
Logging string
|
||||
HopID uint
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error { return fmt.Errorf("not implemented") },
|
||||
}, {
|
||||
ID: "31",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Model(&dbmodels.Host{}).Updates(&dbmodels.Host{Logging: "everything"}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error { return fmt.Errorf("not implemented") },
|
||||
}, {
|
||||
ID: "32",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type ACL struct {
|
||||
gorm.Model
|
||||
HostGroups []*dbmodels.HostGroup `gorm:"many2many:host_group_acls;"`
|
||||
UserGroups []*dbmodels.UserGroup `gorm:"many2many:user_group_acls;"`
|
||||
HostPattern string `valid:"optional"`
|
||||
Action string `valid:"required"`
|
||||
Weight uint ``
|
||||
Comment string `valid:"optional"`
|
||||
Inception *time.Time
|
||||
Expiration *time.Time
|
||||
}
|
||||
return tx.AutoMigrate(&ACL{}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error { return fmt.Errorf("not implemented") },
|
||||
},
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
return err
|
||||
}
|
||||
NewEvent("system", "migrated").Log(db)
|
||||
dbmodels.NewEvent("system", "migrated").Log(db)
|
||||
|
||||
// create default ssh key
|
||||
var count uint
|
||||
@@ -495,7 +537,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 +553,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 +567,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 +581,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 +599,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 +610,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 +625,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 +644,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 +656,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 +685,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
@@ -0,0 +1,71 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type logTunnel struct {
|
||||
host string
|
||||
channel ssh.Channel
|
||||
writer io.WriteCloser
|
||||
}
|
||||
|
||||
type logTunnelForwardData struct {
|
||||
DestinationHost string
|
||||
DestinationPort uint32
|
||||
SourceHost string
|
||||
SourcePort uint32
|
||||
}
|
||||
|
||||
func writeHeader(fd io.Writer, length int) {
|
||||
t := time.Now()
|
||||
|
||||
tv := syscall.NsecToTimeval(t.UnixNano())
|
||||
|
||||
if err := binary.Write(fd, binary.LittleEndian, int32(tv.Sec)); err != nil {
|
||||
log.Printf("failed to write log header: %v", err)
|
||||
}
|
||||
if err := binary.Write(fd, binary.LittleEndian, tv.Usec); err != nil {
|
||||
log.Printf("failed to write log header: %v", err)
|
||||
}
|
||||
if err := binary.Write(fd, binary.LittleEndian, int32(length)); err != nil {
|
||||
log.Printf("failed to write log header: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newLogTunnel(channel ssh.Channel, writer io.WriteCloser, host string) io.ReadWriteCloser {
|
||||
return &logTunnel{
|
||||
host: host,
|
||||
channel: channel,
|
||||
writer: writer,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logTunnel) Read(data []byte) (int, error) {
|
||||
return 0, errors.New("logTunnel.Read is not implemented")
|
||||
}
|
||||
|
||||
func (l *logTunnel) Write(data []byte) (int, error) {
|
||||
writeHeader(l.writer, len(data)+len(l.host+": "))
|
||||
if _, err := l.writer.Write([]byte(l.host + ": ")); err != nil {
|
||||
log.Printf("failed to write log: %v", err)
|
||||
}
|
||||
if _, err := l.writer.Write(data); err != nil {
|
||||
log.Printf("failed to write log: %v", err)
|
||||
}
|
||||
|
||||
return l.channel.Write(data)
|
||||
}
|
||||
|
||||
func (l *logTunnel) Close() error {
|
||||
l.writer.Close()
|
||||
|
||||
return l.channel.Close()
|
||||
}
|
||||
264
pkg/bastion/session.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sabban/bastion/pkg/logchannel"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type sessionConfig struct {
|
||||
Addr string
|
||||
LogsLocation string
|
||||
ClientConfig *gossh.ClientConfig
|
||||
LoggingMode string
|
||||
}
|
||||
|
||||
func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []sessionConfig, sessionID uint) error {
|
||||
var lastClient *gossh.Client
|
||||
switch newChan.ChannelType() {
|
||||
case "session":
|
||||
lch, lreqs, err := newChan.Accept()
|
||||
// TODO: defer clean closer
|
||||
if err != nil {
|
||||
// TODO: trigger event callback
|
||||
return nil
|
||||
}
|
||||
|
||||
// go through all the hops
|
||||
for _, config := range configs {
|
||||
var client *gossh.Client
|
||||
if lastClient == nil {
|
||||
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
|
||||
} else {
|
||||
rconn, err := lastClient.Dial("tcp", config.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ncc, chans, reqs, err := gossh.NewClientConn(rconn, config.Addr, config.ClientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client = gossh.NewClient(ncc, chans, reqs)
|
||||
}
|
||||
if err != nil {
|
||||
lch.Close() // fix #56
|
||||
return err
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
lastClient = client
|
||||
}
|
||||
|
||||
rch, rreqs, err := lastClient.OpenChannel("session", []byte{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := conn.User()
|
||||
actx := ctx.Value(authContextKey).(*authContext)
|
||||
username := actx.user.Name
|
||||
// pipe everything
|
||||
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1], user, username, sessionID, newChan)
|
||||
case "direct-tcpip":
|
||||
lch, lreqs, err := newChan.Accept()
|
||||
// TODO: defer clean closer
|
||||
if err != nil {
|
||||
// TODO: trigger event callback
|
||||
return nil
|
||||
}
|
||||
|
||||
// go through all the hops
|
||||
for _, config := range configs {
|
||||
var client *gossh.Client
|
||||
if lastClient == nil {
|
||||
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
|
||||
} else {
|
||||
rconn, err := lastClient.Dial("tcp", config.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ncc, chans, reqs, err := gossh.NewClientConn(rconn, config.Addr, config.ClientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client = gossh.NewClient(ncc, chans, reqs)
|
||||
}
|
||||
if err != nil {
|
||||
lch.Close()
|
||||
return err
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
lastClient = client
|
||||
}
|
||||
|
||||
d := logTunnelForwardData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
return err
|
||||
}
|
||||
rch, rreqs, err := lastClient.OpenChannel("direct-tcpip", newChan.ExtraData())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := conn.User()
|
||||
actx := ctx.Value(authContextKey).(*authContext)
|
||||
username := actx.user.Name
|
||||
// pipe everything
|
||||
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1], user, username, sessionID, newChan)
|
||||
default:
|
||||
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
|
||||
log.Printf("failed to reject chan: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, sessConfig sessionConfig, user string, username string, sessionID uint, newChan gossh.NewChannel) error {
|
||||
defer func() {
|
||||
_ = lch.Close()
|
||||
_ = rch.Close()
|
||||
}()
|
||||
|
||||
errch := make(chan error, 1)
|
||||
quit := make(chan string, 1)
|
||||
channeltype := newChan.ChannelType()
|
||||
|
||||
var logWriter io.WriteCloser = newDiscardWriteCloser()
|
||||
if sessConfig.LoggingMode != "disabled" {
|
||||
filename := filepath.Join(sessConfig.LogsLocation, fmt.Sprintf("%s-%s-%s-%d-%s", user, username, channeltype, sessionID, time.Now().Format(time.RFC3339)))
|
||||
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0440)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "open log file")
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
log.Printf("Session %v is recorded in %v", channeltype, filename)
|
||||
logWriter = f
|
||||
}
|
||||
|
||||
if channeltype == "session" {
|
||||
switch sessConfig.LoggingMode {
|
||||
case "input":
|
||||
wrappedrch := logchannel.New(rch, logWriter)
|
||||
go func(quit chan string) {
|
||||
_, _ = io.Copy(lch, rch)
|
||||
quit <- "rch"
|
||||
}(quit)
|
||||
go func(quit chan string) {
|
||||
_, _ = io.Copy(wrappedrch, lch)
|
||||
quit <- "lch"
|
||||
}(quit)
|
||||
default: // everything, disabled
|
||||
wrappedlch := logchannel.New(lch, logWriter)
|
||||
go func(quit chan string) {
|
||||
_, _ = io.Copy(wrappedlch, rch)
|
||||
quit <- "rch"
|
||||
}(quit)
|
||||
go func(quit chan string) {
|
||||
_, _ = io.Copy(rch, lch)
|
||||
quit <- "lch"
|
||||
}(quit)
|
||||
}
|
||||
}
|
||||
if channeltype == "direct-tcpip" {
|
||||
d := logTunnelForwardData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
return err
|
||||
}
|
||||
wrappedlch := newLogTunnel(lch, logWriter, d.SourceHost)
|
||||
wrappedrch := newLogTunnel(rch, logWriter, d.DestinationHost)
|
||||
go func(quit chan string) {
|
||||
_, _ = io.Copy(wrappedlch, rch)
|
||||
quit <- "rch"
|
||||
}(quit)
|
||||
|
||||
go func(quit chan string) {
|
||||
_, _ = io.Copy(wrappedrch, lch)
|
||||
quit <- "lch"
|
||||
}(quit)
|
||||
}
|
||||
|
||||
go func(quit chan string) {
|
||||
for req := range lreqs {
|
||||
b, err := rch.SendRequest(req.Type, req.WantReply, req.Payload)
|
||||
if req.Type == "exec" {
|
||||
wrappedlch := logchannel.New(lch, logWriter)
|
||||
command := append(req.Payload, []byte("\n")...)
|
||||
if _, err := wrappedlch.LogWrite(command); err != nil {
|
||||
log.Printf("failed to write log: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errch <- err
|
||||
}
|
||||
if err2 := req.Reply(b, nil); err2 != nil {
|
||||
errch <- err2
|
||||
}
|
||||
}
|
||||
quit <- "lreqs"
|
||||
}(quit)
|
||||
|
||||
go func(quit chan string) {
|
||||
for req := range rreqs {
|
||||
b, err := lch.SendRequest(req.Type, req.WantReply, req.Payload)
|
||||
if err != nil {
|
||||
errch <- err
|
||||
}
|
||||
if err2 := req.Reply(b, nil); err2 != nil {
|
||||
errch <- err2
|
||||
}
|
||||
}
|
||||
quit <- "rreqs"
|
||||
}(quit)
|
||||
|
||||
lchEOF, rchEOF, lchClosed, rchClosed := false, false, false, false
|
||||
for {
|
||||
select {
|
||||
case err := <-errch:
|
||||
return err
|
||||
case q := <-quit:
|
||||
switch q {
|
||||
case "lch":
|
||||
lchEOF = true
|
||||
_ = rch.CloseWrite()
|
||||
case "rch":
|
||||
rchEOF = true
|
||||
_ = lch.CloseWrite()
|
||||
case "lreqs":
|
||||
lchClosed = true
|
||||
case "rreqs":
|
||||
rchClosed = true
|
||||
}
|
||||
|
||||
if lchEOF && lchClosed && !rchClosed {
|
||||
rch.Close()
|
||||
}
|
||||
|
||||
if rchEOF && rchClosed && !lchClosed {
|
||||
lch.Close()
|
||||
}
|
||||
|
||||
if lchEOF && rchEOF && lchClosed && rchClosed {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newDiscardWriteCloser() io.WriteCloser { return &discardWriteCloser{ioutil.Discard} }
|
||||
|
||||
type discardWriteCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (discardWriteCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -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,26 @@ 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,
|
||||
LogsLocation: actx.logsLocation,
|
||||
LoggingMode: currentHost.Logging,
|
||||
}}, 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 +149,29 @@ 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(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,
|
||||
}
|
||||
if err == nil {
|
||||
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,35 @@ 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)
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
|
||||
HostDecrypt(actx.config.aesKey, host)
|
||||
SSHKeyDecrypt(actx.config.aesKey, host.SSHKey)
|
||||
|
||||
action := checkACLs(tmpUser, tmpHost)
|
||||
switch action {
|
||||
case ACLActionAllow:
|
||||
case ACLActionDeny:
|
||||
case string(dbmodels.ACLActionAllow):
|
||||
// do nothing
|
||||
case string(dbmodels.ACLActionDeny):
|
||||
return nil, fmt.Errorf("you don't have permission to that host")
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid ACL action: %q", action)
|
||||
@@ -230,10 +218,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 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 +235,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); 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 +287,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 +307,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 +334,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 +342,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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -1,201 +0,0 @@
|
||||
package bastionsession
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/arkan/bastion/pkg/logchannel"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/moul/sshportal/pkg/logtunnel"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type ForwardData struct {
|
||||
DestinationHost string
|
||||
DestinationPort uint32
|
||||
SourceHost string
|
||||
SourcePort uint32
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Addr string
|
||||
Logs string
|
||||
ClientConfig *gossh.ClientConfig
|
||||
}
|
||||
|
||||
func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []Config) error {
|
||||
var lastClient *gossh.Client
|
||||
switch newChan.ChannelType() {
|
||||
case "session":
|
||||
lch, lreqs, err := newChan.Accept()
|
||||
// TODO: defer clean closer
|
||||
if err != nil {
|
||||
// TODO: trigger event callback
|
||||
return nil
|
||||
}
|
||||
|
||||
// go through all the hops
|
||||
for _, config := range configs {
|
||||
var client *gossh.Client
|
||||
if lastClient == nil {
|
||||
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
|
||||
} else {
|
||||
rconn, err := lastClient.Dial("tcp", config.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ncc, chans, reqs, err := gossh.NewClientConn(rconn, config.Addr, config.ClientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client = gossh.NewClient(ncc, chans, reqs)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
lastClient = client
|
||||
}
|
||||
|
||||
rch, rreqs, err := lastClient.OpenChannel("session", []byte{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := conn.User()
|
||||
// pipe everything
|
||||
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, newChan)
|
||||
case "direct-tcpip":
|
||||
lch, lreqs, err := newChan.Accept()
|
||||
// TODO: defer clean closer
|
||||
if err != nil {
|
||||
// TODO: trigger event callback
|
||||
return nil
|
||||
}
|
||||
|
||||
// go through all the hops
|
||||
for _, config := range configs {
|
||||
var client *gossh.Client
|
||||
if lastClient == nil {
|
||||
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
|
||||
} else {
|
||||
rconn, err := lastClient.Dial("tcp", config.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ncc, chans, reqs, err := gossh.NewClientConn(rconn, config.Addr, config.ClientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client = gossh.NewClient(ncc, chans, reqs)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
lastClient = client
|
||||
}
|
||||
|
||||
d := logtunnel.ForwardData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
return err
|
||||
}
|
||||
rch, rreqs, err := lastClient.OpenChannel("direct-tcpip", newChan.ExtraData())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := conn.User()
|
||||
// pipe everything
|
||||
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, newChan)
|
||||
default:
|
||||
newChan.Reject(gossh.UnknownChannelType, "unsupported channel type")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocation string, user string, newChan gossh.NewChannel) error {
|
||||
defer func() {
|
||||
_ = lch.Close()
|
||||
_ = rch.Close()
|
||||
}()
|
||||
|
||||
errch := make(chan error, 1)
|
||||
channeltype := newChan.ChannelType()
|
||||
|
||||
file_name := strings.Join([]string{logsLocation, "/", user, "-", channeltype, "-", time.Now().Format(time.RFC3339)}, "") // get user
|
||||
f, err := os.OpenFile(file_name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640)
|
||||
defer f.Close()
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("error: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Session %v is recorded in %v", channeltype, file_name)
|
||||
if channeltype == "session" {
|
||||
wrappedlch := logchannel.New(lch, f)
|
||||
go func() {
|
||||
_, _ = io.Copy(wrappedlch, rch)
|
||||
errch <- errors.New("lch closed the connection")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(rch, lch)
|
||||
errch <- errors.New("rch closed the connection")
|
||||
}()
|
||||
}
|
||||
if channeltype == "direct-tcpip" {
|
||||
d := logtunnel.ForwardData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
return err
|
||||
}
|
||||
wrappedlch := logtunnel.New(lch, f, d.SourceHost)
|
||||
wrappedrch := logtunnel.New(rch, f, d.DestinationHost)
|
||||
go func() {
|
||||
_, _ = io.Copy(wrappedlch, rch)
|
||||
errch <- errors.New("lch closed the connection")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(wrappedrch, lch)
|
||||
errch <- errors.New("rch closed the connection")
|
||||
}()
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case req := <-lreqs: // forward ssh requests from local to remote
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := rch.SendRequest(req.Type, req.WantReply, req.Payload)
|
||||
if req.Type == "exec" {
|
||||
wrappedlch := logchannel.New(lch, f)
|
||||
command := append(req.Payload, []byte("\n")...)
|
||||
wrappedlch.LogWrite(command)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err2 := req.Reply(b, nil); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
case req := <-rreqs: // forward ssh requests from remote to local
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := lch.SendRequest(req.Type, req.WantReply, req.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err2 := req.Reply(b, nil); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
case err := <-errch:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
package main
|
||||
package dbmodels // import "moul.io/sshportal/pkg/dbmodels"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/jinzhu/gorm"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
@@ -40,12 +38,12 @@ type Setting struct {
|
||||
type SSHKey struct {
|
||||
// FIXME: use uuid for ID
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Name string `valid:"required,length(1|255),unix_user"`
|
||||
Type string `valid:"required"`
|
||||
Length uint `valid:"required"`
|
||||
Fingerprint string `valid:"optional"`
|
||||
PrivKey string `sql:"size:10000" valid:"required"`
|
||||
PubKey string `sql:"size:10000" valid:"optional"`
|
||||
PrivKey string `sql:"size:5000" valid:"required"`
|
||||
PubKey string `sql:"size:1000" valid:"optional"`
|
||||
Hosts []*Host `gorm:"ForeignKey:SSHKeyID"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
@@ -53,16 +51,17 @@ 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:255" valid:"required,length(1|255)"`
|
||||
Addr string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
|
||||
User string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
|
||||
Password string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
|
||||
URL string `valid:"optional"`
|
||||
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000" valid:"optional"`
|
||||
HostKey []byte `sql:"size:1000" valid:"optional"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string `valid:"optional"`
|
||||
Logging string `valid:"optional,host_logging_mode"`
|
||||
Hop *Host
|
||||
HopID uint
|
||||
}
|
||||
@@ -70,8 +69,8 @@ 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)"`
|
||||
AuthorizedKey string `sql:"size:10000" valid:"required,length(1|10000)"`
|
||||
Key []byte `sql:"size:1000" valid:"length(1|1000)"`
|
||||
AuthorizedKey string `sql:"size:1000" valid:"required,length(1|1000)"`
|
||||
UserID uint ``
|
||||
User *User `gorm:"ForeignKey:UserID"`
|
||||
Comment string `valid:"optional"`
|
||||
@@ -79,7 +78,7 @@ type UserKey struct {
|
||||
|
||||
type UserRole struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Name string `valid:"required,length(1|255),unix_user"`
|
||||
Users []*User `gorm:"many2many:user_user_roles"`
|
||||
}
|
||||
|
||||
@@ -88,7 +87,7 @@ 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"`
|
||||
Name string `valid:"required,length(1|255),unix_user"`
|
||||
Keys []*UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string `valid:"optional"`
|
||||
@@ -97,7 +96,7 @@ type User struct {
|
||||
|
||||
type UserGroup struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Name string `valid:"required,length(1|255),unix_user"`
|
||||
Users []*User `gorm:"many2many:user_user_groups;"`
|
||||
ACLs []*ACL `gorm:"many2many:user_group_acls;"`
|
||||
Comment string `valid:"optional"`
|
||||
@@ -105,7 +104,7 @@ type UserGroup struct {
|
||||
|
||||
type HostGroup struct {
|
||||
gorm.Model
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Name string `valid:"required,length(1|255),unix_user"`
|
||||
Hosts []*Host `gorm:"many2many:host_host_groups;"`
|
||||
ACLs []*ACL `gorm:"many2many:host_group_acls;"`
|
||||
Comment string `valid:"optional"`
|
||||
@@ -119,6 +118,8 @@ type ACL struct {
|
||||
Action string `valid:"required"`
|
||||
Weight uint ``
|
||||
Comment string `valid:"optional"`
|
||||
Inception *time.Time
|
||||
Expiration *time.Time
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
@@ -148,49 +149,26 @@ 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() {
|
||||
unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*")
|
||||
|
||||
govalidator.CustomTypeTagMap.Set("unix_user", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||
name, ok := i.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return unixUserRegexp.MatchString(name)
|
||||
}))
|
||||
}
|
||||
|
||||
// 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 +275,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 +374,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 +425,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
|
||||
}
|
||||
33
pkg/dbmodels/validator.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package dbmodels
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
func InitValidator() {
|
||||
unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*")
|
||||
|
||||
govalidator.CustomTypeTagMap.Set("unix_user", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||
name, ok := i.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return unixUserRegexp.MatchString(name)
|
||||
}))
|
||||
govalidator.CustomTypeTagMap.Set("host_logging_mode", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||
name, ok := i.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if name == "" {
|
||||
return true
|
||||
}
|
||||
return IsValidHostLoggingMode(name)
|
||||
}))
|
||||
}
|
||||
|
||||
func IsValidHostLoggingMode(name string) bool {
|
||||
return name == "disabled" || name == "input" || name == "everything"
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
321
rules.mk
vendored
Normal file
@@ -0,0 +1,321 @@
|
||||
# +--------------------------------------------------------------+
|
||||
# | * * * moul.io/rules.mk |
|
||||
# +--------------------------------------------------------------+
|
||||
# | |
|
||||
# | ++ ______________________________________ |
|
||||
# | ++++ / \ |
|
||||
# | ++++ | | |
|
||||
# | ++++++++++ | https://moul.io/rules.mk is a set | |
|
||||
# | +++ | | of common Makefile rules that can | |
|
||||
# | ++ | | be configured from the Makefile | |
|
||||
# | + -== ==| | or with environment variables. | |
|
||||
# | ( <*> <*> | | |
|
||||
# | | | /| Manfred Touron | |
|
||||
# | | _) / | manfred.life | |
|
||||
# | | +++ / \______________________________________/ |
|
||||
# | \ =+ / |
|
||||
# | \ + |
|
||||
# | |\++++++ |
|
||||
# | | ++++ ||// |
|
||||
# | ___| |___ _||/__ __|
|
||||
# | / --- \ \| ||| __ _ ___ __ __/ /|
|
||||
# |/ | | \ \ / / ' \/ _ \/ // / / |
|
||||
# || | | | | | /_/_/_/\___/\_,_/_/ |
|
||||
# +--------------------------------------------------------------+
|
||||
|
||||
all: help
|
||||
|
||||
##
|
||||
## Common helpers
|
||||
##
|
||||
|
||||
rwildcard = $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
|
||||
check-program = $(foreach exec,$(1),$(if $(shell PATH="$(PATH)" which $(exec)),,$(error "No $(exec) in PATH")))
|
||||
|
||||
##
|
||||
## rules.mk
|
||||
##
|
||||
ifneq ($(wildcard rules.mk),)
|
||||
.PHONY: rulesmk.bumpdeps
|
||||
rulesmk.bumpdeps:
|
||||
wget -O rules.mk https://raw.githubusercontent.com/moul/rules.mk/master/rules.mk
|
||||
BUMPDEPS_STEPS += rulesmk.bumpdeps
|
||||
endif
|
||||
|
||||
##
|
||||
## Maintainer
|
||||
##
|
||||
|
||||
ifneq ($(wildcard .git/HEAD),)
|
||||
.PHONY: generate.authors
|
||||
generate.authors: AUTHORS
|
||||
AUTHORS: .git/
|
||||
echo "# This file lists all individuals having contributed content to the repository." > AUTHORS
|
||||
echo "# For how it is generated, see 'https://github.com/moul/rules.mk'" >> AUTHORS
|
||||
echo >> AUTHORS
|
||||
git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf >> AUTHORS
|
||||
GENERATE_STEPS += generate.authors
|
||||
endif
|
||||
|
||||
##
|
||||
## Golang
|
||||
##
|
||||
|
||||
ifndef GOPKG
|
||||
ifneq ($(wildcard go.mod),)
|
||||
GOPKG = $(shell sed '/module/!d;s/^omdule\ //' go.mod)
|
||||
endif
|
||||
endif
|
||||
ifdef GOPKG
|
||||
GO ?= go
|
||||
GOPATH ?= $(HOME)/go
|
||||
GO_INSTALL_OPTS ?=
|
||||
GO_TEST_OPTS ?= -test.timeout=30s
|
||||
GOMOD_DIR ?= .
|
||||
GOCOVERAGE_FILE ?= ./coverage.txt
|
||||
GOTESTJSON_FILE ?= ./go-test.json
|
||||
GOBUILDLOG_FILE ?= ./go-build.log
|
||||
GOINSTALLLOG_FILE ?= ./go-install.log
|
||||
|
||||
ifdef GOBINS
|
||||
.PHONY: go.install
|
||||
go.install:
|
||||
ifeq ($(CI),true)
|
||||
@rm -f /tmp/goinstall.log
|
||||
@set -e; for dir in $(GOBINS); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) install -v $(GO_INSTALL_OPTS) .; \
|
||||
); done 2>&1 | tee $(GOINSTALLLOG_FILE)
|
||||
|
||||
else
|
||||
@set -e; for dir in $(GOBINS); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) install $(GO_INSTALL_OPTS) .; \
|
||||
); done
|
||||
endif
|
||||
INSTALL_STEPS += go.install
|
||||
|
||||
.PHONY: go.release
|
||||
go.release:
|
||||
$(call check-program, goreleaser)
|
||||
goreleaser --snapshot --skip-publish --rm-dist
|
||||
@echo -n "Do you want to release? [y/N] " && read ans && \
|
||||
if [ $${ans:-N} = y ]; then set -xe; goreleaser --rm-dist; fi
|
||||
RELEASE_STEPS += go.release
|
||||
endif
|
||||
|
||||
.PHONY: go.unittest
|
||||
go.unittest:
|
||||
ifeq ($(CI),true)
|
||||
@echo "mode: atomic" > /tmp/gocoverage
|
||||
@rm -f $(GOTESTJSON_FILE)
|
||||
@set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do (set -e; (set -euf pipefail; \
|
||||
cd $$dir; \
|
||||
($(GO) test ./... $(GO_TEST_OPTS) -cover -coverprofile=/tmp/profile.out -covermode=atomic -race -json | tee -a $(GOTESTJSON_FILE) 3>&1 1>&2 2>&3 | tee -a $(GOBUILDLOG_FILE); \
|
||||
); \
|
||||
if [ -f /tmp/profile.out ]; then \
|
||||
cat /tmp/profile.out | sed "/mode: atomic/d" >> /tmp/gocoverage; \
|
||||
rm -f /tmp/profile.out; \
|
||||
fi)); done
|
||||
@mv /tmp/gocoverage $(GOCOVERAGE_FILE)
|
||||
else
|
||||
@echo "mode: atomic" > /tmp/gocoverage
|
||||
@set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do (set -e; (set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) test ./... $(GO_TEST_OPTS) -cover -coverprofile=/tmp/profile.out -covermode=atomic -race); \
|
||||
if [ -f /tmp/profile.out ]; then \
|
||||
cat /tmp/profile.out | sed "/mode: atomic/d" >> /tmp/gocoverage; \
|
||||
rm -f /tmp/profile.out; \
|
||||
fi); done
|
||||
@mv /tmp/gocoverage $(GOCOVERAGE_FILE)
|
||||
endif
|
||||
|
||||
.PHONY: go.checkdoc
|
||||
go.checkdoc:
|
||||
go doc $(GOMOD_DIR)
|
||||
|
||||
.PHONY: go.coverfunc
|
||||
go.coverfunc: go.unittest
|
||||
go tool cover -func=$(GOCOVERAGE_FILE) | grep -v .pb.go: | grep -v .pb.gw.go:
|
||||
|
||||
.PHONY: go.lint
|
||||
go.lint:
|
||||
@set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do ( set -xe; \
|
||||
cd $$dir; \
|
||||
golangci-lint run --verbose ./...; \
|
||||
); done
|
||||
|
||||
.PHONY: go.tidy
|
||||
go.tidy:
|
||||
@set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) mod tidy; \
|
||||
); done
|
||||
|
||||
.PHONY: go.build
|
||||
go.build:
|
||||
@set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) build ./...; \
|
||||
); done
|
||||
|
||||
.PHONY: go.bump-deps
|
||||
go.bumpdeps:
|
||||
@set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do ( set -xe; \
|
||||
cd $$dir; \
|
||||
$(GO) get -u ./...; \
|
||||
); done
|
||||
|
||||
.PHONY: go.bump-deps
|
||||
go.fmt:
|
||||
if ! command -v goimports &>/dev/null; then GO111MODULE=off go get golang.org/x/tools/cmd/goimports; fi
|
||||
@set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do ( set -xe; \
|
||||
cd $$dir; \
|
||||
goimports -w `go list -f '{{.Dir}}' ./...)` \
|
||||
); done
|
||||
|
||||
BUILD_STEPS += go.build
|
||||
BUMPDEPS_STEPS += go.bumpdeps
|
||||
TIDY_STEPS += go.tidy
|
||||
LINT_STEPS += go.lint
|
||||
UNITTEST_STEPS += go.unittest
|
||||
FMT_STEPS += go.fmt
|
||||
endif
|
||||
|
||||
##
|
||||
## Gitattributes
|
||||
##
|
||||
|
||||
ifneq ($(wildcard .gitattributes),)
|
||||
.PHONY: _linguist-ignored
|
||||
_linguist-kept:
|
||||
@git check-attr linguist-vendored $(shell git check-attr linguist-generated $(shell find . -type f | grep -v .git/) | grep unspecified | cut -d: -f1) | grep unspecified | cut -d: -f1 | sort
|
||||
|
||||
.PHONY: _linguist-kept
|
||||
_linguist-ignored:
|
||||
@git check-attr linguist-vendored linguist-ignored `find . -not -path './.git/*' -type f` | grep '\ set$$' | cut -d: -f1 | sort -u
|
||||
endif
|
||||
|
||||
##
|
||||
## Node
|
||||
##
|
||||
|
||||
ifndef NPM_PACKAGES
|
||||
ifneq ($(wildcard package.json),)
|
||||
NPM_PACKAGES = .
|
||||
endif
|
||||
endif
|
||||
ifdef NPM_PACKAGES
|
||||
.PHONY: npm.publish
|
||||
npm.publish:
|
||||
@echo -n "Do you want to npm publish? [y/N] " && read ans && \
|
||||
@if [ $${ans:-N} = y ]; then \
|
||||
set -e; for dir in $(NPM_PACKAGES); do ( set -xe; \
|
||||
cd $$dir; \
|
||||
npm publish --access=public; \
|
||||
); done; \
|
||||
fi
|
||||
RELEASE_STEPS += npm.publish
|
||||
endif
|
||||
|
||||
##
|
||||
## Docker
|
||||
##
|
||||
|
||||
docker_build = docker build \
|
||||
--build-arg VCS_REF=`git rev-parse --short HEAD` \
|
||||
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
|
||||
--build-arg VERSION=`git describe --tags --always` \
|
||||
-t "$2" -f "$1" "$(dir $1)"
|
||||
|
||||
ifndef DOCKERFILE_PATH
|
||||
DOCKERFILE_PATH = ./Dockerfile
|
||||
endif
|
||||
ifndef DOCKER_IMAGE
|
||||
ifneq ($(wildcard Dockerfile),)
|
||||
DOCKER_IMAGE = $(notdir $(PWD))
|
||||
endif
|
||||
endif
|
||||
ifdef DOCKER_IMAGE
|
||||
ifneq ($(DOCKER_IMAGE),none)
|
||||
.PHONY: docker.build
|
||||
docker.build:
|
||||
$(call check-program, docker)
|
||||
$(call docker_build,$(DOCKERFILE_PATH),$(DOCKER_IMAGE))
|
||||
|
||||
BUILD_STEPS += docker.build
|
||||
endif
|
||||
endif
|
||||
|
||||
##
|
||||
## Common
|
||||
##
|
||||
|
||||
TEST_STEPS += $(UNITTEST_STEPS)
|
||||
TEST_STEPS += $(LINT_STEPS)
|
||||
TEST_STEPS += $(TIDY_STEPS)
|
||||
|
||||
ifneq ($(strip $(TEST_STEPS)),)
|
||||
.PHONY: test
|
||||
test: $(PRE_TEST_STEPS) $(TEST_STEPS)
|
||||
endif
|
||||
|
||||
ifdef INSTALL_STEPS
|
||||
.PHONY: install
|
||||
install: $(PRE_INSTALL_STEPS) $(INSTALL_STEPS)
|
||||
endif
|
||||
|
||||
ifdef UNITTEST_STEPS
|
||||
.PHONY: unittest
|
||||
unittest: $(PRE_UNITTEST_STEPS) $(UNITTEST_STEPS)
|
||||
endif
|
||||
|
||||
ifdef LINT_STEPS
|
||||
.PHONY: lint
|
||||
lint: $(PRE_LINT_STEPS) $(FMT_STEPS) $(LINT_STEPS)
|
||||
endif
|
||||
|
||||
ifdef TIDY_STEPS
|
||||
.PHONY: tidy
|
||||
tidy: $(PRE_TIDY_STEPS) $(TIDY_STEPS)
|
||||
endif
|
||||
|
||||
ifdef BUILD_STEPS
|
||||
.PHONY: build
|
||||
build: $(PRE_BUILD_STEPS) $(BUILD_STEPS)
|
||||
endif
|
||||
|
||||
ifdef RELEASE_STEPS
|
||||
.PHONY: release
|
||||
release: $(PRE_RELEASE_STEPS) $(RELEASE_STEPS)
|
||||
endif
|
||||
|
||||
ifdef BUMPDEPS_STEPS
|
||||
.PHONY: bumpdeps
|
||||
bumpdeps: $(PRE_BUMDEPS_STEPS) $(BUMPDEPS_STEPS)
|
||||
endif
|
||||
|
||||
ifdef FMT_STEPS
|
||||
.PHONY: fmt
|
||||
fmt: $(PRE_FMT_STEPS) $(FMT_STEPS)
|
||||
endif
|
||||
|
||||
ifdef GENERATE_STEPS
|
||||
.PHONY: generate
|
||||
generate: $(PRE_GENERATE_STEPS) $(GENERATE_STEPS)
|
||||
endif
|
||||
|
||||
.PHONY: help
|
||||
help::
|
||||
@echo "General commands:"
|
||||
@[ "$(BUILD_STEPS)" != "" ] && echo " build" || true
|
||||
@[ "$(BUMPDEPS_STEPS)" != "" ] && echo " bumpdeps" || true
|
||||
@[ "$(FMT_STEPS)" != "" ] && echo " fmt" || true
|
||||
@[ "$(GENERATE_STEPS)" != "" ] && echo " generate" || true
|
||||
@[ "$(INSTALL_STEPS)" != "" ] && echo " install" || true
|
||||
@[ "$(LINT_STEPS)" != "" ] && echo " lint" || true
|
||||
@[ "$(RELEASE_STEPS)" != "" ] && echo " release" || true
|
||||
@[ "$(TEST_STEPS)" != "" ] && echo " test" || true
|
||||
@[ "$(TIDY_STEPS)" != "" ] && echo " tidy" || true
|
||||
@[ "$(UNITTEST_STEPS)" != "" ] && echo " unittest" || true
|
||||
@# FIXME: list other commands
|
||||
134
server.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/urfave/cli"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"moul.io/sshportal/pkg/bastion"
|
||||
)
|
||||
|
||||
type serverConfig struct {
|
||||
aesKey string
|
||||
dbDriver, dbURL string
|
||||
logsLocation string
|
||||
bindAddr string
|
||||
debug, demo bool
|
||||
idleTimeout time.Duration
|
||||
}
|
||||
|
||||
func parseServerConfig(c *cli.Context) (*serverConfig, error) {
|
||||
ret := &serverConfig{
|
||||
aesKey: c.String("aes-key"),
|
||||
dbDriver: c.String("db-driver"),
|
||||
dbURL: c.String("db-conn"),
|
||||
bindAddr: c.String("bind-address"),
|
||||
debug: c.Bool("debug"),
|
||||
demo: c.Bool("demo"),
|
||||
logsLocation: c.String("logs-location"),
|
||||
idleTimeout: c.Duration("idle-timeout"),
|
||||
}
|
||||
switch len(ret.aesKey) {
|
||||
case 0, 16, 24, 32:
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid aes key size, should be 16 or 24, 32")
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func ensureLogDirectory(location string) error {
|
||||
// check for the logdir existence
|
||||
logsLocation, err := os.Stat(location)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return os.MkdirAll(location, os.ModeDir|os.FileMode(0750))
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !logsLocation.IsDir() {
|
||||
return fmt.Errorf("log directory cannot be created")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func server(c *serverConfig) (err error) {
|
||||
var db = (*gorm.DB)(nil)
|
||||
|
||||
// try to setup the local DB
|
||||
if db, err = gorm.Open(c.dbDriver, c.dbURL); err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
origErr := err
|
||||
err = db.Close()
|
||||
if origErr != nil {
|
||||
err = origErr
|
||||
}
|
||||
}()
|
||||
if err = db.DB().Ping(); err != nil {
|
||||
return
|
||||
}
|
||||
db.LogMode(c.debug)
|
||||
if err = bastion.DBInit(db); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// create TCP listening socket
|
||||
ln, err := net.Listen("tcp", c.bindAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// configure server
|
||||
srv := &ssh.Server{
|
||||
Addr: c.bindAddr,
|
||||
Handler: func(s ssh.Session) { bastion.ShellHandler(s, GitTag, GitSha, GitTag) }, // ssh.Server.Handler is the handler for the DefaultSessionHandler
|
||||
Version: fmt.Sprintf("sshportal-%s", GitTag),
|
||||
ChannelHandlers: map[string]ssh.ChannelHandler{
|
||||
"default": bastion.ChannelHandler,
|
||||
},
|
||||
}
|
||||
|
||||
// configure channel handler
|
||||
bastion.DefaultChannelHandler = func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
|
||||
switch newChan.ChannelType() {
|
||||
case "session":
|
||||
go ssh.DefaultSessionHandler(srv, conn, newChan, ctx)
|
||||
case "direct-tcpip":
|
||||
go ssh.DirectTCPIPHandler(srv, conn, newChan, ctx)
|
||||
default:
|
||||
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
|
||||
log.Printf("failed to reject chan: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.idleTimeout != 0 {
|
||||
srv.IdleTimeout = c.idleTimeout
|
||||
// gliderlabs/ssh requires MaxTimeout to be non-zero if we want to use IdleTimeout.
|
||||
// So, set it to the max value, because we don't want a max timeout.
|
||||
srv.MaxTimeout = math.MaxInt64
|
||||
}
|
||||
|
||||
for _, opt := range []ssh.Option{
|
||||
// custom PublicKeyAuth handler
|
||||
ssh.PublicKeyAuth(bastion.PublicKeyAuthHandler(db, c.logsLocation, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
|
||||
ssh.PasswordAuth(bastion.PasswordAuthHandler(db, c.logsLocation, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
|
||||
// retrieve sshportal SSH private key from database
|
||||
bastion.PrivateKeyFromDB(db, c.aesKey),
|
||||
} {
|
||||
if err := srv.SetOption(opt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("info: SSH Server accepting connections on %s, idle-timout=%v", c.bindAddr, c.idleTimeout)
|
||||
return srv.Serve(ln)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
14
testserver_unsupported.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// testServer is an hidden handler used for integration tests
|
||||
func testServer(c *cli.Context) error {
|
||||
return fmt.Errorf("not available on windows")
|
||||
}
|
||||
20
util.go
@@ -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] + "..."
|
||||
}
|
||||
20
vendor/github.com/anmitsu/go-shlex/LICENSE
generated
vendored
@@ -1,20 +0,0 @@
|
||||
Copyright (c) anmitsu <anmitsu.s@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
38
vendor/github.com/anmitsu/go-shlex/README.md
generated
vendored
@@ -1,38 +0,0 @@
|
||||
# go-shlex
|
||||
|
||||
go-shlex is a library to make a lexical analyzer like Unix shell for
|
||||
Go.
|
||||
|
||||
## Install
|
||||
|
||||
go get -u "github.com/anmitsu/go-shlex"
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/anmitsu/go-shlex"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd := `cp -Rdp "file name" 'file name2' dir\ name`
|
||||
words, err := shlex.Split(cmd, true)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, w := range words {
|
||||
fmt.Println(w)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
http://godoc.org/github.com/anmitsu/go-shlex
|
||||
|
||||
193
vendor/github.com/anmitsu/go-shlex/shlex.go
generated
vendored
@@ -1,193 +0,0 @@
|
||||
// Package shlex provides a simple lexical analysis like Unix shell.
|
||||
package shlex
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoClosing = errors.New("No closing quotation")
|
||||
ErrNoEscaped = errors.New("No escaped character")
|
||||
)
|
||||
|
||||
// Tokenizer is the interface that classifies a token according to
|
||||
// words, whitespaces, quotations, escapes and escaped quotations.
|
||||
type Tokenizer interface {
|
||||
IsWord(rune) bool
|
||||
IsWhitespace(rune) bool
|
||||
IsQuote(rune) bool
|
||||
IsEscape(rune) bool
|
||||
IsEscapedQuote(rune) bool
|
||||
}
|
||||
|
||||
// DefaultTokenizer implements a simple tokenizer like Unix shell.
|
||||
type DefaultTokenizer struct{}
|
||||
|
||||
func (t *DefaultTokenizer) IsWord(r rune) bool {
|
||||
return r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r)
|
||||
}
|
||||
func (t *DefaultTokenizer) IsQuote(r rune) bool {
|
||||
switch r {
|
||||
case '\'', '"':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
func (t *DefaultTokenizer) IsWhitespace(r rune) bool {
|
||||
return unicode.IsSpace(r)
|
||||
}
|
||||
func (t *DefaultTokenizer) IsEscape(r rune) bool {
|
||||
return r == '\\'
|
||||
}
|
||||
func (t *DefaultTokenizer) IsEscapedQuote(r rune) bool {
|
||||
return r == '"'
|
||||
}
|
||||
|
||||
// Lexer represents a lexical analyzer.
|
||||
type Lexer struct {
|
||||
reader *bufio.Reader
|
||||
tokenizer Tokenizer
|
||||
posix bool
|
||||
whitespacesplit bool
|
||||
}
|
||||
|
||||
// NewLexer creates a new Lexer reading from io.Reader. This Lexer
|
||||
// has a DefaultTokenizer according to posix and whitespacesplit
|
||||
// rules.
|
||||
func NewLexer(r io.Reader, posix, whitespacesplit bool) *Lexer {
|
||||
return &Lexer{
|
||||
reader: bufio.NewReader(r),
|
||||
tokenizer: &DefaultTokenizer{},
|
||||
posix: posix,
|
||||
whitespacesplit: whitespacesplit,
|
||||
}
|
||||
}
|
||||
|
||||
// NewLexerString creates a new Lexer reading from a string. This
|
||||
// Lexer has a DefaultTokenizer according to posix and whitespacesplit
|
||||
// rules.
|
||||
func NewLexerString(s string, posix, whitespacesplit bool) *Lexer {
|
||||
return NewLexer(strings.NewReader(s), posix, whitespacesplit)
|
||||
}
|
||||
|
||||
// Split splits a string according to posix or non-posix rules.
|
||||
func Split(s string, posix bool) ([]string, error) {
|
||||
return NewLexerString(s, posix, true).Split()
|
||||
}
|
||||
|
||||
// SetTokenizer sets a Tokenizer.
|
||||
func (l *Lexer) SetTokenizer(t Tokenizer) {
|
||||
l.tokenizer = t
|
||||
}
|
||||
|
||||
func (l *Lexer) Split() ([]string, error) {
|
||||
result := make([]string, 0)
|
||||
for {
|
||||
token, err := l.readToken()
|
||||
if token != "" {
|
||||
result = append(result, token)
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (l *Lexer) readToken() (string, error) {
|
||||
t := l.tokenizer
|
||||
token := ""
|
||||
quoted := false
|
||||
state := ' '
|
||||
escapedstate := ' '
|
||||
scanning:
|
||||
for {
|
||||
next, _, err := l.reader.ReadRune()
|
||||
if err != nil {
|
||||
if t.IsQuote(state) {
|
||||
return token, ErrNoClosing
|
||||
} else if t.IsEscape(state) {
|
||||
return token, ErrNoEscaped
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case t.IsWhitespace(state):
|
||||
switch {
|
||||
case t.IsWhitespace(next):
|
||||
break scanning
|
||||
case l.posix && t.IsEscape(next):
|
||||
escapedstate = 'a'
|
||||
state = next
|
||||
case t.IsWord(next):
|
||||
token += string(next)
|
||||
state = 'a'
|
||||
case t.IsQuote(next):
|
||||
if !l.posix {
|
||||
token += string(next)
|
||||
}
|
||||
state = next
|
||||
default:
|
||||
token = string(next)
|
||||
if l.whitespacesplit {
|
||||
state = 'a'
|
||||
} else if token != "" || (l.posix && quoted) {
|
||||
break scanning
|
||||
}
|
||||
}
|
||||
case t.IsQuote(state):
|
||||
quoted = true
|
||||
switch {
|
||||
case next == state:
|
||||
if !l.posix {
|
||||
token += string(next)
|
||||
break scanning
|
||||
} else {
|
||||
state = 'a'
|
||||
}
|
||||
case l.posix && t.IsEscape(next) && t.IsEscapedQuote(state):
|
||||
escapedstate = state
|
||||
state = next
|
||||
default:
|
||||
token += string(next)
|
||||
}
|
||||
case t.IsEscape(state):
|
||||
if t.IsQuote(escapedstate) && next != state && next != escapedstate {
|
||||
token += string(state)
|
||||
}
|
||||
token += string(next)
|
||||
state = escapedstate
|
||||
case t.IsWord(state):
|
||||
switch {
|
||||
case t.IsWhitespace(next):
|
||||
if token != "" || (l.posix && quoted) {
|
||||
break scanning
|
||||
}
|
||||
case l.posix && t.IsQuote(next):
|
||||
state = next
|
||||
case l.posix && t.IsEscape(next):
|
||||
escapedstate = 'a'
|
||||
state = next
|
||||
case t.IsWord(next) || t.IsQuote(next):
|
||||
token += string(next)
|
||||
default:
|
||||
if l.whitespacesplit {
|
||||
token += string(next)
|
||||
} else if token != "" {
|
||||
l.reader.UnreadRune()
|
||||
break scanning
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
22
vendor/github.com/arkan/bastion/LICENSE
generated
vendored
@@ -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.
|
||||
|
||||
54
vendor/github.com/arkan/bastion/pkg/logchannel/logchannel.go
generated
vendored
@@ -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()
|
||||
}
|
||||
21
vendor/github.com/asaskevich/govalidator/LICENSE
generated
vendored
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Alex Saskevich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
410
vendor/github.com/asaskevich/govalidator/README.md
generated
vendored
@@ -1,410 +0,0 @@
|
||||
govalidator
|
||||
===========
|
||||
[](https://gitter.im/asaskevich/govalidator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://godoc.org/github.com/asaskevich/govalidator) [](https://coveralls.io/r/asaskevich/govalidator?branch=master) [](https://app.wercker.com/project/bykey/1ec990b09ea86c910d5f08b0e02c6043)
|
||||
[](https://travis-ci.org/asaskevich/govalidator)
|
||||
|
||||
A package of validators and sanitizers for strings, structs and collections. Based on [validator.js](https://github.com/chriso/validator.js).
|
||||
|
||||
#### Installation
|
||||
Make sure that Go is installed on your computer.
|
||||
Type the following command in your terminal:
|
||||
|
||||
go get github.com/asaskevich/govalidator
|
||||
|
||||
or you can get specified release of the package with `gopkg.in`:
|
||||
|
||||
go get gopkg.in/asaskevich/govalidator.v4
|
||||
|
||||
After it the package is ready to use.
|
||||
|
||||
|
||||
#### Import package in your project
|
||||
Add following line in your `*.go` file:
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
```
|
||||
If you are unhappy to use long `govalidator`, you can do something like this:
|
||||
```go
|
||||
import (
|
||||
valid "github.com/asaskevich/govalidator"
|
||||
)
|
||||
```
|
||||
|
||||
#### Activate behavior to require all fields have a validation tag by default
|
||||
`SetFieldsRequiredByDefault` causes validation to fail when struct fields do not include validations or are not explicitly marked as exempt (using `valid:"-"` or `valid:"email,optional"`). A good place to activate this is a package init function or the main() function.
|
||||
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
|
||||
func init() {
|
||||
govalidator.SetFieldsRequiredByDefault(true)
|
||||
}
|
||||
```
|
||||
|
||||
Here's some code to explain it:
|
||||
```go
|
||||
// this struct definition will fail govalidator.ValidateStruct() (and the field values do not matter):
|
||||
type exampleStruct struct {
|
||||
Name string ``
|
||||
Email string `valid:"email"`
|
||||
}
|
||||
|
||||
// this, however, will only fail when Email is empty or an invalid email address:
|
||||
type exampleStruct2 struct {
|
||||
Name string `valid:"-"`
|
||||
Email string `valid:"email"`
|
||||
}
|
||||
|
||||
// lastly, this will only fail when Email is an invalid email address but not when it's empty:
|
||||
type exampleStruct2 struct {
|
||||
Name string `valid:"-"`
|
||||
Email string `valid:"email,optional"`
|
||||
}
|
||||
```
|
||||
|
||||
#### Recent breaking changes (see [#123](https://github.com/asaskevich/govalidator/pull/123))
|
||||
##### Custom validator function signature
|
||||
A context was added as the second parameter, for structs this is the object being validated – this makes dependent validation possible.
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
|
||||
// old signature
|
||||
func(i interface{}) bool
|
||||
|
||||
// new signature
|
||||
func(i interface{}, o interface{}) bool
|
||||
```
|
||||
|
||||
##### Adding a custom validator
|
||||
This was changed to prevent data races when accessing custom validators.
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
|
||||
// before
|
||||
govalidator.CustomTypeTagMap["customByteArrayValidator"] = CustomTypeValidator(func(i interface{}, o interface{}) bool {
|
||||
// ...
|
||||
})
|
||||
|
||||
// after
|
||||
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", CustomTypeValidator(func(i interface{}, o interface{}) bool {
|
||||
// ...
|
||||
}))
|
||||
```
|
||||
|
||||
#### List of functions:
|
||||
```go
|
||||
func Abs(value float64) float64
|
||||
func BlackList(str, chars string) string
|
||||
func ByteLength(str string, params ...string) bool
|
||||
func StringLength(str string, params ...string) bool
|
||||
func StringMatches(s string, params ...string) bool
|
||||
func CamelCaseToUnderscore(str string) string
|
||||
func Contains(str, substring string) bool
|
||||
func Count(array []interface{}, iterator ConditionIterator) int
|
||||
func Each(array []interface{}, iterator Iterator)
|
||||
func ErrorByField(e error, field string) string
|
||||
func Filter(array []interface{}, iterator ConditionIterator) []interface{}
|
||||
func Find(array []interface{}, iterator ConditionIterator) interface{}
|
||||
func GetLine(s string, index int) (string, error)
|
||||
func GetLines(s string) []string
|
||||
func IsHost(s string) bool
|
||||
func InRange(value, left, right float64) bool
|
||||
func IsASCII(str string) bool
|
||||
func IsAlpha(str string) bool
|
||||
func IsAlphanumeric(str string) bool
|
||||
func IsBase64(str string) bool
|
||||
func IsByteLength(str string, min, max int) bool
|
||||
func IsCreditCard(str string) bool
|
||||
func IsDataURI(str string) bool
|
||||
func IsDialString(str string) bool
|
||||
func IsDNSName(str string) bool
|
||||
func IsDivisibleBy(str, num string) bool
|
||||
func IsEmail(str string) bool
|
||||
func IsFilePath(str string) (bool, int)
|
||||
func IsFloat(str string) bool
|
||||
func IsFullWidth(str string) bool
|
||||
func IsHalfWidth(str string) bool
|
||||
func IsHexadecimal(str string) bool
|
||||
func IsHexcolor(str string) bool
|
||||
func IsIP(str string) bool
|
||||
func IsIPv4(str string) bool
|
||||
func IsIPv6(str string) bool
|
||||
func IsISBN(str string, version int) bool
|
||||
func IsISBN10(str string) bool
|
||||
func IsISBN13(str string) bool
|
||||
func IsISO3166Alpha2(str string) bool
|
||||
func IsISO3166Alpha3(str string) bool
|
||||
func IsInt(str string) bool
|
||||
func IsIn(str string, params ...string) bool
|
||||
func IsJSON(str string) bool
|
||||
func IsLatitude(str string) bool
|
||||
func IsLongitude(str string) bool
|
||||
func IsLowerCase(str string) bool
|
||||
func IsMAC(str string) bool
|
||||
func IsMongoID(str string) bool
|
||||
func IsMultibyte(str string) bool
|
||||
func IsNatural(value float64) bool
|
||||
func IsNegative(value float64) bool
|
||||
func IsNonNegative(value float64) bool
|
||||
func IsNonPositive(value float64) bool
|
||||
func IsNull(str string) bool
|
||||
func IsNumeric(str string) bool
|
||||
func IsPort(str string) bool
|
||||
func IsPositive(value float64) bool
|
||||
func IsPrintableASCII(str string) bool
|
||||
func IsRGBcolor(str string) bool
|
||||
func IsRequestURI(rawurl string) bool
|
||||
func IsRequestURL(rawurl string) bool
|
||||
func IsSSN(str string) bool
|
||||
func IsSemver(str string) bool
|
||||
func IsURL(str string) bool
|
||||
func IsUTFDigit(str string) bool
|
||||
func IsUTFLetter(str string) bool
|
||||
func IsUTFLetterNumeric(str string) bool
|
||||
func IsUTFNumeric(str string) bool
|
||||
func IsUUID(str string) bool
|
||||
func IsUUIDv3(str string) bool
|
||||
func IsUUIDv4(str string) bool
|
||||
func IsUUIDv5(str string) bool
|
||||
func IsUpperCase(str string) bool
|
||||
func IsVariableWidth(str string) bool
|
||||
func IsWhole(value float64) bool
|
||||
func LeftTrim(str, chars string) string
|
||||
func Map(array []interface{}, iterator ResultIterator) []interface{}
|
||||
func Matches(str, pattern string) bool
|
||||
func NormalizeEmail(str string) (string, error)
|
||||
func PadBoth(str string, padStr string, padLen int) string
|
||||
func PadLeft(str string, padStr string, padLen int) string
|
||||
func PadRight(str string, padStr string, padLen int) string
|
||||
func RemoveTags(s string) string
|
||||
func ReplacePattern(str, pattern, replace string) string
|
||||
func Reverse(s string) string
|
||||
func RightTrim(str, chars string) string
|
||||
func SafeFileName(str string) string
|
||||
func Sign(value float64) float64
|
||||
func StripLow(str string, keepNewLines bool) string
|
||||
func ToBoolean(str string) (bool, error)
|
||||
func ToFloat(str string) (float64, error)
|
||||
func ToInt(str string) (int64, error)
|
||||
func ToJSON(obj interface{}) (string, error)
|
||||
func ToString(obj interface{}) string
|
||||
func Trim(str, chars string) string
|
||||
func Truncate(str string, length int, ending string) string
|
||||
func UnderscoreToCamelCase(s string) string
|
||||
func ValidateStruct(s interface{}) (bool, error)
|
||||
func WhiteList(str, chars string) string
|
||||
type ConditionIterator
|
||||
type Error
|
||||
func (e Error) Error() string
|
||||
type Errors
|
||||
func (es Errors) Error() string
|
||||
type ISO3166Entry
|
||||
type Iterator
|
||||
type ParamValidator
|
||||
type ResultIterator
|
||||
type UnsupportedTypeError
|
||||
func (e *UnsupportedTypeError) Error() string
|
||||
type Validator
|
||||
```
|
||||
|
||||
#### Examples
|
||||
###### IsURL
|
||||
```go
|
||||
println(govalidator.IsURL(`http://user@pass:domain.com/path/page`))
|
||||
```
|
||||
###### ToString
|
||||
```go
|
||||
type User struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
str := govalidator.ToString(&User{"John", "Juan"})
|
||||
println(str)
|
||||
```
|
||||
###### Each, Map, Filter, Count for slices
|
||||
Each iterates over the slice/array and calls Iterator for every item
|
||||
```go
|
||||
data := []interface{}{1, 2, 3, 4, 5}
|
||||
var fn govalidator.Iterator = func(value interface{}, index int) {
|
||||
println(value.(int))
|
||||
}
|
||||
govalidator.Each(data, fn)
|
||||
```
|
||||
```go
|
||||
data := []interface{}{1, 2, 3, 4, 5}
|
||||
var fn govalidator.ResultIterator = func(value interface{}, index int) interface{} {
|
||||
return value.(int) * 3
|
||||
}
|
||||
_ = govalidator.Map(data, fn) // result = []interface{}{1, 6, 9, 12, 15}
|
||||
```
|
||||
```go
|
||||
data := []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
var fn govalidator.ConditionIterator = func(value interface{}, index int) bool {
|
||||
return value.(int)%2 == 0
|
||||
}
|
||||
_ = govalidator.Filter(data, fn) // result = []interface{}{2, 4, 6, 8, 10}
|
||||
_ = govalidator.Count(data, fn) // result = 5
|
||||
```
|
||||
###### ValidateStruct [#2](https://github.com/asaskevich/govalidator/pull/2)
|
||||
If you want to validate structs, you can use tag `valid` for any field in your structure. All validators used with this field in one tag are separated by comma. If you want to skip validation, place `-` in your tag. If you need a validator that is not on the list below, you can add it like this:
|
||||
```go
|
||||
govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
|
||||
return str == "duck"
|
||||
})
|
||||
```
|
||||
For completely custom validators (interface-based), see below.
|
||||
|
||||
Here is a list of available validators for struct fields (validator - used function):
|
||||
```go
|
||||
"email": IsEmail,
|
||||
"url": IsURL,
|
||||
"dialstring": IsDialString,
|
||||
"requrl": IsRequestURL,
|
||||
"requri": IsRequestURI,
|
||||
"alpha": IsAlpha,
|
||||
"utfletter": IsUTFLetter,
|
||||
"alphanum": IsAlphanumeric,
|
||||
"utfletternum": IsUTFLetterNumeric,
|
||||
"numeric": IsNumeric,
|
||||
"utfnumeric": IsUTFNumeric,
|
||||
"utfdigit": IsUTFDigit,
|
||||
"hexadecimal": IsHexadecimal,
|
||||
"hexcolor": IsHexcolor,
|
||||
"rgbcolor": IsRGBcolor,
|
||||
"lowercase": IsLowerCase,
|
||||
"uppercase": IsUpperCase,
|
||||
"int": IsInt,
|
||||
"float": IsFloat,
|
||||
"null": IsNull,
|
||||
"uuid": IsUUID,
|
||||
"uuidv3": IsUUIDv3,
|
||||
"uuidv4": IsUUIDv4,
|
||||
"uuidv5": IsUUIDv5,
|
||||
"creditcard": IsCreditCard,
|
||||
"isbn10": IsISBN10,
|
||||
"isbn13": IsISBN13,
|
||||
"json": IsJSON,
|
||||
"multibyte": IsMultibyte,
|
||||
"ascii": IsASCII,
|
||||
"printableascii": IsPrintableASCII,
|
||||
"fullwidth": IsFullWidth,
|
||||
"halfwidth": IsHalfWidth,
|
||||
"variablewidth": IsVariableWidth,
|
||||
"base64": IsBase64,
|
||||
"datauri": IsDataURI,
|
||||
"ip": IsIP,
|
||||
"port": IsPort,
|
||||
"ipv4": IsIPv4,
|
||||
"ipv6": IsIPv6,
|
||||
"dns": IsDNSName,
|
||||
"host": IsHost,
|
||||
"mac": IsMAC,
|
||||
"latitude": IsLatitude,
|
||||
"longitude": IsLongitude,
|
||||
"ssn": IsSSN,
|
||||
"semver": IsSemver,
|
||||
"rfc3339": IsRFC3339,
|
||||
"ISO3166Alpha2": IsISO3166Alpha2,
|
||||
"ISO3166Alpha3": IsISO3166Alpha3,
|
||||
```
|
||||
Validators with parameters
|
||||
|
||||
```go
|
||||
"length(min|max)": ByteLength,
|
||||
"runelength(min|max)": RuneLegth,
|
||||
"matches(pattern)": StringMatches,
|
||||
"in(string1|string2|...|stringN)": IsIn,
|
||||
```
|
||||
|
||||
And here is small example of usage:
|
||||
```go
|
||||
type Post struct {
|
||||
Title string `valid:"alphanum,required"`
|
||||
Message string `valid:"duck,ascii"`
|
||||
AuthorIP string `valid:"ipv4"`
|
||||
Date string `valid:"-"`
|
||||
}
|
||||
post := &Post{
|
||||
Title: "My Example Post",
|
||||
Message: "duck",
|
||||
AuthorIP: "123.234.54.3",
|
||||
}
|
||||
|
||||
// Add your own struct validation tags
|
||||
govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
|
||||
return str == "duck"
|
||||
})
|
||||
|
||||
result, err := govalidator.ValidateStruct(post)
|
||||
if err != nil {
|
||||
println("error: " + err.Error())
|
||||
}
|
||||
println(result)
|
||||
```
|
||||
###### WhiteList
|
||||
```go
|
||||
// Remove all characters from string ignoring characters between "a" and "z"
|
||||
println(govalidator.WhiteList("a3a43a5a4a3a2a23a4a5a4a3a4", "a-z") == "aaaaaaaaaaaa")
|
||||
```
|
||||
|
||||
###### Custom validation functions
|
||||
Custom validation using your own domain specific validators is also available - here's an example of how to use it:
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
|
||||
type CustomByteArray [6]byte // custom types are supported and can be validated
|
||||
|
||||
type StructWithCustomByteArray struct {
|
||||
ID CustomByteArray `valid:"customByteArrayValidator,customMinLengthValidator"` // multiple custom validators are possible as well and will be evaluated in sequence
|
||||
Email string `valid:"email"`
|
||||
CustomMinLength int `valid:"-"`
|
||||
}
|
||||
|
||||
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||
switch v := context.(type) { // you can type switch on the context interface being validated
|
||||
case StructWithCustomByteArray:
|
||||
// you can check and validate against some other field in the context,
|
||||
// return early or not validate against the context at all – your choice
|
||||
case SomeOtherType:
|
||||
// ...
|
||||
default:
|
||||
// expecting some other type? Throw/panic here or continue
|
||||
}
|
||||
|
||||
switch v := i.(type) { // type switch on the struct field being validated
|
||||
case CustomByteArray:
|
||||
for _, e := range v { // this validator checks that the byte array is not empty, i.e. not all zeroes
|
||||
if e != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}))
|
||||
govalidator.CustomTypeTagMap.Set("customMinLengthValidator", CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||
switch v := context.(type) { // this validates a field against the value in another field, i.e. dependent validation
|
||||
case StructWithCustomByteArray:
|
||||
return len(v.ID) >= v.CustomMinLength
|
||||
}
|
||||
return false
|
||||
}))
|
||||
```
|
||||
|
||||
#### Notes
|
||||
Documentation is available here: [godoc.org](https://godoc.org/github.com/asaskevich/govalidator).
|
||||
Full information about code coverage is also available here: [govalidator on gocover.io](http://gocover.io/github.com/asaskevich/govalidator).
|
||||
|
||||
#### Support
|
||||
If you do have a contribution for the package feel free to put up a Pull Request or open Issue.
|
||||
|
||||
#### Special thanks to [contributors](https://github.com/asaskevich/govalidator/graphs/contributors)
|
||||
* [Daniel Lohse](https://github.com/annismckenzie)
|
||||
* [Attila Oláh](https://github.com/attilaolah)
|
||||
* [Daniel Korner](https://github.com/Dadie)
|
||||
* [Steven Wilkin](https://github.com/stevenwilkin)
|
||||
* [Deiwin Sarjas](https://github.com/deiwin)
|
||||
* [Noah Shibley](https://github.com/slugmobile)
|
||||
* [Nathan Davies](https://github.com/nathj07)
|
||||
* [Matt Sanford](https://github.com/mzsanford)
|
||||
* [Simon ccl1115](https://github.com/ccl1115)
|
||||
58
vendor/github.com/asaskevich/govalidator/arrays.go
generated
vendored
@@ -1,58 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
// Iterator is the function that accepts element of slice/array and its index
|
||||
type Iterator func(interface{}, int)
|
||||
|
||||
// ResultIterator is the function that accepts element of slice/array and its index and returns any result
|
||||
type ResultIterator func(interface{}, int) interface{}
|
||||
|
||||
// ConditionIterator is the function that accepts element of slice/array and its index and returns boolean
|
||||
type ConditionIterator func(interface{}, int) bool
|
||||
|
||||
// Each iterates over the slice and apply Iterator to every item
|
||||
func Each(array []interface{}, iterator Iterator) {
|
||||
for index, data := range array {
|
||||
iterator(data, index)
|
||||
}
|
||||
}
|
||||
|
||||
// Map iterates over the slice and apply ResultIterator to every item. Returns new slice as a result.
|
||||
func Map(array []interface{}, iterator ResultIterator) []interface{} {
|
||||
var result = make([]interface{}, len(array))
|
||||
for index, data := range array {
|
||||
result[index] = iterator(data, index)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Find iterates over the slice and apply ConditionIterator to every item. Returns first item that meet ConditionIterator or nil otherwise.
|
||||
func Find(array []interface{}, iterator ConditionIterator) interface{} {
|
||||
for index, data := range array {
|
||||
if iterator(data, index) {
|
||||
return data
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter iterates over the slice and apply ConditionIterator to every item. Returns new slice.
|
||||
func Filter(array []interface{}, iterator ConditionIterator) []interface{} {
|
||||
var result = make([]interface{}, 0)
|
||||
for index, data := range array {
|
||||
if iterator(data, index) {
|
||||
result = append(result, data)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Count iterates over the slice and apply ConditionIterator to every item. Returns count of items that meets ConditionIterator.
|
||||
func Count(array []interface{}, iterator ConditionIterator) int {
|
||||
count := 0
|
||||
for index, data := range array {
|
||||
if iterator(data, index) {
|
||||
count = count + 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
49
vendor/github.com/asaskevich/govalidator/converter.go
generated
vendored
@@ -1,49 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ToString convert the input to a string.
|
||||
func ToString(obj interface{}) string {
|
||||
res := fmt.Sprintf("%v", obj)
|
||||
return string(res)
|
||||
}
|
||||
|
||||
// ToJSON convert the input to a valid JSON string
|
||||
func ToJSON(obj interface{}) (string, error) {
|
||||
res, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
res = []byte("")
|
||||
}
|
||||
return string(res), err
|
||||
}
|
||||
|
||||
// ToFloat convert the input string to a float, or 0.0 if the input is not a float.
|
||||
func ToFloat(str string) (float64, error) {
|
||||
res, err := strconv.ParseFloat(str, 64)
|
||||
if err != nil {
|
||||
res = 0.0
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ToInt convert the input string to an integer, or 0 if the input is not an integer.
|
||||
func ToInt(str string) (int64, error) {
|
||||
res, err := strconv.ParseInt(str, 0, 64)
|
||||
if err != nil {
|
||||
res = 0
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ToBoolean convert the input string to a boolean.
|
||||
func ToBoolean(str string) (bool, error) {
|
||||
res, err := strconv.ParseBool(str)
|
||||
if err != nil {
|
||||
res = false
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
31
vendor/github.com/asaskevich/govalidator/error.go
generated
vendored
@@ -1,31 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
// Errors is an array of multiple errors and conforms to the error interface.
|
||||
type Errors []error
|
||||
|
||||
// Errors returns itself.
|
||||
func (es Errors) Errors() []error {
|
||||
return es
|
||||
}
|
||||
|
||||
func (es Errors) Error() string {
|
||||
var err string
|
||||
for _, e := range es {
|
||||
err += e.Error() + ";"
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Error encapsulates a name, an error and whether there's a custom error message or not.
|
||||
type Error struct {
|
||||
Name string
|
||||
Err error
|
||||
CustomErrorMessageExists bool
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
if e.CustomErrorMessageExists {
|
||||
return e.Err.Error()
|
||||
}
|
||||
return e.Name + ": " + e.Err.Error()
|
||||
}
|
||||
57
vendor/github.com/asaskevich/govalidator/numerics.go
generated
vendored
@@ -1,57 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
import "math"
|
||||
|
||||
// Abs returns absolute value of number
|
||||
func Abs(value float64) float64 {
|
||||
return value * Sign(value)
|
||||
}
|
||||
|
||||
// Sign returns signum of number: 1 in case of value > 0, -1 in case of value < 0, 0 otherwise
|
||||
func Sign(value float64) float64 {
|
||||
if value > 0 {
|
||||
return 1
|
||||
} else if value < 0 {
|
||||
return -1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// IsNegative returns true if value < 0
|
||||
func IsNegative(value float64) bool {
|
||||
return value < 0
|
||||
}
|
||||
|
||||
// IsPositive returns true if value > 0
|
||||
func IsPositive(value float64) bool {
|
||||
return value > 0
|
||||
}
|
||||
|
||||
// IsNonNegative returns true if value >= 0
|
||||
func IsNonNegative(value float64) bool {
|
||||
return value >= 0
|
||||
}
|
||||
|
||||
// IsNonPositive returns true if value <= 0
|
||||
func IsNonPositive(value float64) bool {
|
||||
return value <= 0
|
||||
}
|
||||
|
||||
// InRange returns true if value lies between left and right border
|
||||
func InRange(value, left, right float64) bool {
|
||||
if left > right {
|
||||
left, right = right, left
|
||||
}
|
||||
return value >= left && value <= right
|
||||
}
|
||||
|
||||
// IsWhole returns true if value is whole number
|
||||
func IsWhole(value float64) bool {
|
||||
return Abs(math.Remainder(value, 1)) == 0
|
||||
}
|
||||
|
||||
// IsNatural returns true if value is natural number (positive and whole)
|
||||
func IsNatural(value float64) bool {
|
||||
return IsWhole(value) && IsPositive(value)
|
||||
}
|
||||
91
vendor/github.com/asaskevich/govalidator/patterns.go
generated
vendored
@@ -1,91 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
import "regexp"
|
||||
|
||||
// Basic regular expressions for validating strings
|
||||
const (
|
||||
Email string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
|
||||
CreditCard string = "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$"
|
||||
ISBN10 string = "^(?:[0-9]{9}X|[0-9]{10})$"
|
||||
ISBN13 string = "^(?:[0-9]{13})$"
|
||||
UUID3 string = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
UUID4 string = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||
UUID5 string = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||
UUID string = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
Alpha string = "^[a-zA-Z]+$"
|
||||
Alphanumeric string = "^[a-zA-Z0-9]+$"
|
||||
Numeric string = "^[-+]?[0-9]+$"
|
||||
Int string = "^(?:[-+]?(?:0|[1-9][0-9]*))$"
|
||||
Float string = "^(?:[-+]?(?:[0-9]+))?(?:\\.[0-9]*)?(?:[eE][\\+\\-]?(?:[0-9]+))?$"
|
||||
Hexadecimal string = "^[0-9a-fA-F]+$"
|
||||
Hexcolor string = "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
|
||||
RGBcolor string = "^rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*\\)$"
|
||||
ASCII string = "^[\x00-\x7F]+$"
|
||||
Multibyte string = "[^\x00-\x7F]"
|
||||
FullWidth string = "[^\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
|
||||
HalfWidth string = "[\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
|
||||
Base64 string = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
|
||||
PrintableASCII string = "^[\x20-\x7E]+$"
|
||||
DataURI string = "^data:.+\\/(.+);base64$"
|
||||
Latitude string = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$"
|
||||
Longitude string = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$"
|
||||
DNSName string = `^([a-zA-Z0-9]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9]{1}[a-zA-Z0-9_-]{1,62})*$`
|
||||
IP string = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
|
||||
URLSchema string = `((ftp|tcp|udp|wss?|https?):\/\/)`
|
||||
URLUsername string = `(\S+(:\S*)?@)`
|
||||
Hostname string = ``
|
||||
URLPath string = `((\/|\?|#)[^\s]*)`
|
||||
URLPort string = `(:(\d{1,5}))`
|
||||
URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3])(\.(1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))`
|
||||
URLSubdomain string = `((www\.)|([a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*))`
|
||||
URL string = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))` + URLPort + `?` + URLPath + `?$`
|
||||
SSN string = `^\d{3}[- ]?\d{2}[- ]?\d{4}$`
|
||||
WinPath string = `^[a-zA-Z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$`
|
||||
UnixPath string = `^(/[^/\x00]*)+/?$`
|
||||
Semver string = "^v?(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(-(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\\+[0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*)?$"
|
||||
tagName string = "valid"
|
||||
)
|
||||
|
||||
// Used by IsFilePath func
|
||||
const (
|
||||
// Unknown is unresolved OS type
|
||||
Unknown = iota
|
||||
// Win is Windows type
|
||||
Win
|
||||
// Unix is *nix OS types
|
||||
Unix
|
||||
)
|
||||
|
||||
var (
|
||||
rxEmail = regexp.MustCompile(Email)
|
||||
rxCreditCard = regexp.MustCompile(CreditCard)
|
||||
rxISBN10 = regexp.MustCompile(ISBN10)
|
||||
rxISBN13 = regexp.MustCompile(ISBN13)
|
||||
rxUUID3 = regexp.MustCompile(UUID3)
|
||||
rxUUID4 = regexp.MustCompile(UUID4)
|
||||
rxUUID5 = regexp.MustCompile(UUID5)
|
||||
rxUUID = regexp.MustCompile(UUID)
|
||||
rxAlpha = regexp.MustCompile(Alpha)
|
||||
rxAlphanumeric = regexp.MustCompile(Alphanumeric)
|
||||
rxNumeric = regexp.MustCompile(Numeric)
|
||||
rxInt = regexp.MustCompile(Int)
|
||||
rxFloat = regexp.MustCompile(Float)
|
||||
rxHexadecimal = regexp.MustCompile(Hexadecimal)
|
||||
rxHexcolor = regexp.MustCompile(Hexcolor)
|
||||
rxRGBcolor = regexp.MustCompile(RGBcolor)
|
||||
rxASCII = regexp.MustCompile(ASCII)
|
||||
rxPrintableASCII = regexp.MustCompile(PrintableASCII)
|
||||
rxMultibyte = regexp.MustCompile(Multibyte)
|
||||
rxFullWidth = regexp.MustCompile(FullWidth)
|
||||
rxHalfWidth = regexp.MustCompile(HalfWidth)
|
||||
rxBase64 = regexp.MustCompile(Base64)
|
||||
rxDataURI = regexp.MustCompile(DataURI)
|
||||
rxLatitude = regexp.MustCompile(Latitude)
|
||||
rxLongitude = regexp.MustCompile(Longitude)
|
||||
rxDNSName = regexp.MustCompile(DNSName)
|
||||
rxURL = regexp.MustCompile(URL)
|
||||
rxSSN = regexp.MustCompile(SSN)
|
||||
rxWinPath = regexp.MustCompile(WinPath)
|
||||
rxUnixPath = regexp.MustCompile(UnixPath)
|
||||
rxSemver = regexp.MustCompile(Semver)
|
||||
)
|
||||
385
vendor/github.com/asaskevich/govalidator/types.go
generated
vendored
@@ -1,385 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Validator is a wrapper for a validator function that returns bool and accepts string.
|
||||
type Validator func(str string) bool
|
||||
|
||||
// CustomTypeValidator is a wrapper for validator functions that returns bool and accepts any type.
|
||||
// The second parameter should be the context (in the case of validating a struct: the whole object being validated).
|
||||
type CustomTypeValidator func(i interface{}, o interface{}) bool
|
||||
|
||||
// ParamValidator is a wrapper for validator functions that accepts additional parameters.
|
||||
type ParamValidator func(str string, params ...string) bool
|
||||
type tagOptionsMap map[string]string
|
||||
|
||||
// UnsupportedTypeError is a wrapper for reflect.Type
|
||||
type UnsupportedTypeError struct {
|
||||
Type reflect.Type
|
||||
}
|
||||
|
||||
// stringValues is a slice of reflect.Value holding *reflect.StringValue.
|
||||
// It implements the methods to sort by string.
|
||||
type stringValues []reflect.Value
|
||||
|
||||
// ParamTagMap is a map of functions accept variants parameters
|
||||
var ParamTagMap = map[string]ParamValidator{
|
||||
"length": ByteLength,
|
||||
"runelength": RuneLength,
|
||||
"stringlength": StringLength,
|
||||
"matches": StringMatches,
|
||||
"in": isInRaw,
|
||||
}
|
||||
|
||||
// ParamTagRegexMap maps param tags to their respective regexes.
|
||||
var ParamTagRegexMap = map[string]*regexp.Regexp{
|
||||
"length": regexp.MustCompile("^length\\((\\d+)\\|(\\d+)\\)$"),
|
||||
"runelength": regexp.MustCompile("^runelength\\((\\d+)\\|(\\d+)\\)$"),
|
||||
"stringlength": regexp.MustCompile("^stringlength\\((\\d+)\\|(\\d+)\\)$"),
|
||||
"in": regexp.MustCompile(`^in\((.*)\)`),
|
||||
"matches": regexp.MustCompile(`^matches\((.+)\)$`),
|
||||
}
|
||||
|
||||
type customTypeTagMap struct {
|
||||
validators map[string]CustomTypeValidator
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (tm *customTypeTagMap) Get(name string) (CustomTypeValidator, bool) {
|
||||
tm.RLock()
|
||||
defer tm.RUnlock()
|
||||
v, ok := tm.validators[name]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (tm *customTypeTagMap) Set(name string, ctv CustomTypeValidator) {
|
||||
tm.Lock()
|
||||
defer tm.Unlock()
|
||||
tm.validators[name] = ctv
|
||||
}
|
||||
|
||||
// CustomTypeTagMap is a map of functions that can be used as tags for ValidateStruct function.
|
||||
// Use this to validate compound or custom types that need to be handled as a whole, e.g.
|
||||
// `type UUID [16]byte` (this would be handled as an array of bytes).
|
||||
var CustomTypeTagMap = &customTypeTagMap{validators: make(map[string]CustomTypeValidator)}
|
||||
|
||||
// TagMap is a map of functions, that can be used as tags for ValidateStruct function.
|
||||
var TagMap = map[string]Validator{
|
||||
"email": IsEmail,
|
||||
"url": IsURL,
|
||||
"dialstring": IsDialString,
|
||||
"requrl": IsRequestURL,
|
||||
"requri": IsRequestURI,
|
||||
"alpha": IsAlpha,
|
||||
"utfletter": IsUTFLetter,
|
||||
"alphanum": IsAlphanumeric,
|
||||
"utfletternum": IsUTFLetterNumeric,
|
||||
"numeric": IsNumeric,
|
||||
"utfnumeric": IsUTFNumeric,
|
||||
"utfdigit": IsUTFDigit,
|
||||
"hexadecimal": IsHexadecimal,
|
||||
"hexcolor": IsHexcolor,
|
||||
"rgbcolor": IsRGBcolor,
|
||||
"lowercase": IsLowerCase,
|
||||
"uppercase": IsUpperCase,
|
||||
"int": IsInt,
|
||||
"float": IsFloat,
|
||||
"null": IsNull,
|
||||
"uuid": IsUUID,
|
||||
"uuidv3": IsUUIDv3,
|
||||
"uuidv4": IsUUIDv4,
|
||||
"uuidv5": IsUUIDv5,
|
||||
"creditcard": IsCreditCard,
|
||||
"isbn10": IsISBN10,
|
||||
"isbn13": IsISBN13,
|
||||
"json": IsJSON,
|
||||
"multibyte": IsMultibyte,
|
||||
"ascii": IsASCII,
|
||||
"printableascii": IsPrintableASCII,
|
||||
"fullwidth": IsFullWidth,
|
||||
"halfwidth": IsHalfWidth,
|
||||
"variablewidth": IsVariableWidth,
|
||||
"base64": IsBase64,
|
||||
"datauri": IsDataURI,
|
||||
"ip": IsIP,
|
||||
"port": IsPort,
|
||||
"ipv4": IsIPv4,
|
||||
"ipv6": IsIPv6,
|
||||
"dns": IsDNSName,
|
||||
"host": IsHost,
|
||||
"mac": IsMAC,
|
||||
"latitude": IsLatitude,
|
||||
"longitude": IsLongitude,
|
||||
"ssn": IsSSN,
|
||||
"semver": IsSemver,
|
||||
"rfc3339": IsRFC3339,
|
||||
"ISO3166Alpha2": IsISO3166Alpha2,
|
||||
"ISO3166Alpha3": IsISO3166Alpha3,
|
||||
}
|
||||
|
||||
// ISO3166Entry stores country codes
|
||||
type ISO3166Entry struct {
|
||||
EnglishShortName string
|
||||
FrenchShortName string
|
||||
Alpha2Code string
|
||||
Alpha3Code string
|
||||
Numeric string
|
||||
}
|
||||
|
||||
//ISO3166List based on https://www.iso.org/obp/ui/#search/code/ Code Type "Officially Assigned Codes"
|
||||
var ISO3166List = []ISO3166Entry{
|
||||
{"Afghanistan", "Afghanistan (l')", "AF", "AFG", "004"},
|
||||
{"Albania", "Albanie (l')", "AL", "ALB", "008"},
|
||||
{"Antarctica", "Antarctique (l')", "AQ", "ATA", "010"},
|
||||
{"Algeria", "Algérie (l')", "DZ", "DZA", "012"},
|
||||
{"American Samoa", "Samoa américaines (les)", "AS", "ASM", "016"},
|
||||
{"Andorra", "Andorre (l')", "AD", "AND", "020"},
|
||||
{"Angola", "Angola (l')", "AO", "AGO", "024"},
|
||||
{"Antigua and Barbuda", "Antigua-et-Barbuda", "AG", "ATG", "028"},
|
||||
{"Azerbaijan", "Azerbaïdjan (l')", "AZ", "AZE", "031"},
|
||||
{"Argentina", "Argentine (l')", "AR", "ARG", "032"},
|
||||
{"Australia", "Australie (l')", "AU", "AUS", "036"},
|
||||
{"Austria", "Autriche (l')", "AT", "AUT", "040"},
|
||||
{"Bahamas (the)", "Bahamas (les)", "BS", "BHS", "044"},
|
||||
{"Bahrain", "Bahreïn", "BH", "BHR", "048"},
|
||||
{"Bangladesh", "Bangladesh (le)", "BD", "BGD", "050"},
|
||||
{"Armenia", "Arménie (l')", "AM", "ARM", "051"},
|
||||
{"Barbados", "Barbade (la)", "BB", "BRB", "052"},
|
||||
{"Belgium", "Belgique (la)", "BE", "BEL", "056"},
|
||||
{"Bermuda", "Bermudes (les)", "BM", "BMU", "060"},
|
||||
{"Bhutan", "Bhoutan (le)", "BT", "BTN", "064"},
|
||||
{"Bolivia (Plurinational State of)", "Bolivie (État plurinational de)", "BO", "BOL", "068"},
|
||||
{"Bosnia and Herzegovina", "Bosnie-Herzégovine (la)", "BA", "BIH", "070"},
|
||||
{"Botswana", "Botswana (le)", "BW", "BWA", "072"},
|
||||
{"Bouvet Island", "Bouvet (l'Île)", "BV", "BVT", "074"},
|
||||
{"Brazil", "Brésil (le)", "BR", "BRA", "076"},
|
||||
{"Belize", "Belize (le)", "BZ", "BLZ", "084"},
|
||||
{"British Indian Ocean Territory (the)", "Indien (le Territoire britannique de l'océan)", "IO", "IOT", "086"},
|
||||
{"Solomon Islands", "Salomon (Îles)", "SB", "SLB", "090"},
|
||||
{"Virgin Islands (British)", "Vierges britanniques (les Îles)", "VG", "VGB", "092"},
|
||||
{"Brunei Darussalam", "Brunéi Darussalam (le)", "BN", "BRN", "096"},
|
||||
{"Bulgaria", "Bulgarie (la)", "BG", "BGR", "100"},
|
||||
{"Myanmar", "Myanmar (le)", "MM", "MMR", "104"},
|
||||
{"Burundi", "Burundi (le)", "BI", "BDI", "108"},
|
||||
{"Belarus", "Bélarus (le)", "BY", "BLR", "112"},
|
||||
{"Cambodia", "Cambodge (le)", "KH", "KHM", "116"},
|
||||
{"Cameroon", "Cameroun (le)", "CM", "CMR", "120"},
|
||||
{"Canada", "Canada (le)", "CA", "CAN", "124"},
|
||||
{"Cabo Verde", "Cabo Verde", "CV", "CPV", "132"},
|
||||
{"Cayman Islands (the)", "Caïmans (les Îles)", "KY", "CYM", "136"},
|
||||
{"Central African Republic (the)", "République centrafricaine (la)", "CF", "CAF", "140"},
|
||||
{"Sri Lanka", "Sri Lanka", "LK", "LKA", "144"},
|
||||
{"Chad", "Tchad (le)", "TD", "TCD", "148"},
|
||||
{"Chile", "Chili (le)", "CL", "CHL", "152"},
|
||||
{"China", "Chine (la)", "CN", "CHN", "156"},
|
||||
{"Taiwan (Province of China)", "Taïwan (Province de Chine)", "TW", "TWN", "158"},
|
||||
{"Christmas Island", "Christmas (l'Île)", "CX", "CXR", "162"},
|
||||
{"Cocos (Keeling) Islands (the)", "Cocos (les Îles)/ Keeling (les Îles)", "CC", "CCK", "166"},
|
||||
{"Colombia", "Colombie (la)", "CO", "COL", "170"},
|
||||
{"Comoros (the)", "Comores (les)", "KM", "COM", "174"},
|
||||
{"Mayotte", "Mayotte", "YT", "MYT", "175"},
|
||||
{"Congo (the)", "Congo (le)", "CG", "COG", "178"},
|
||||
{"Congo (the Democratic Republic of the)", "Congo (la République démocratique du)", "CD", "COD", "180"},
|
||||
{"Cook Islands (the)", "Cook (les Îles)", "CK", "COK", "184"},
|
||||
{"Costa Rica", "Costa Rica (le)", "CR", "CRI", "188"},
|
||||
{"Croatia", "Croatie (la)", "HR", "HRV", "191"},
|
||||
{"Cuba", "Cuba", "CU", "CUB", "192"},
|
||||
{"Cyprus", "Chypre", "CY", "CYP", "196"},
|
||||
{"Czech Republic (the)", "tchèque (la République)", "CZ", "CZE", "203"},
|
||||
{"Benin", "Bénin (le)", "BJ", "BEN", "204"},
|
||||
{"Denmark", "Danemark (le)", "DK", "DNK", "208"},
|
||||
{"Dominica", "Dominique (la)", "DM", "DMA", "212"},
|
||||
{"Dominican Republic (the)", "dominicaine (la République)", "DO", "DOM", "214"},
|
||||
{"Ecuador", "Équateur (l')", "EC", "ECU", "218"},
|
||||
{"El Salvador", "El Salvador", "SV", "SLV", "222"},
|
||||
{"Equatorial Guinea", "Guinée équatoriale (la)", "GQ", "GNQ", "226"},
|
||||
{"Ethiopia", "Éthiopie (l')", "ET", "ETH", "231"},
|
||||
{"Eritrea", "Érythrée (l')", "ER", "ERI", "232"},
|
||||
{"Estonia", "Estonie (l')", "EE", "EST", "233"},
|
||||
{"Faroe Islands (the)", "Féroé (les Îles)", "FO", "FRO", "234"},
|
||||
{"Falkland Islands (the) [Malvinas]", "Falkland (les Îles)/Malouines (les Îles)", "FK", "FLK", "238"},
|
||||
{"South Georgia and the South Sandwich Islands", "Géorgie du Sud-et-les Îles Sandwich du Sud (la)", "GS", "SGS", "239"},
|
||||
{"Fiji", "Fidji (les)", "FJ", "FJI", "242"},
|
||||
{"Finland", "Finlande (la)", "FI", "FIN", "246"},
|
||||
{"Åland Islands", "Åland(les Îles)", "AX", "ALA", "248"},
|
||||
{"France", "France (la)", "FR", "FRA", "250"},
|
||||
{"French Guiana", "Guyane française (la )", "GF", "GUF", "254"},
|
||||
{"French Polynesia", "Polynésie française (la)", "PF", "PYF", "258"},
|
||||
{"French Southern Territories (the)", "Terres australes françaises (les)", "TF", "ATF", "260"},
|
||||
{"Djibouti", "Djibouti", "DJ", "DJI", "262"},
|
||||
{"Gabon", "Gabon (le)", "GA", "GAB", "266"},
|
||||
{"Georgia", "Géorgie (la)", "GE", "GEO", "268"},
|
||||
{"Gambia (the)", "Gambie (la)", "GM", "GMB", "270"},
|
||||
{"Palestine, State of", "Palestine, État de", "PS", "PSE", "275"},
|
||||
{"Germany", "Allemagne (l')", "DE", "DEU", "276"},
|
||||
{"Ghana", "Ghana (le)", "GH", "GHA", "288"},
|
||||
{"Gibraltar", "Gibraltar", "GI", "GIB", "292"},
|
||||
{"Kiribati", "Kiribati", "KI", "KIR", "296"},
|
||||
{"Greece", "Grèce (la)", "GR", "GRC", "300"},
|
||||
{"Greenland", "Groenland (le)", "GL", "GRL", "304"},
|
||||
{"Grenada", "Grenade (la)", "GD", "GRD", "308"},
|
||||
{"Guadeloupe", "Guadeloupe (la)", "GP", "GLP", "312"},
|
||||
{"Guam", "Guam", "GU", "GUM", "316"},
|
||||
{"Guatemala", "Guatemala (le)", "GT", "GTM", "320"},
|
||||
{"Guinea", "Guinée (la)", "GN", "GIN", "324"},
|
||||
{"Guyana", "Guyana (le)", "GY", "GUY", "328"},
|
||||
{"Haiti", "Haïti", "HT", "HTI", "332"},
|
||||
{"Heard Island and McDonald Islands", "Heard-et-Îles MacDonald (l'Île)", "HM", "HMD", "334"},
|
||||
{"Holy See (the)", "Saint-Siège (le)", "VA", "VAT", "336"},
|
||||
{"Honduras", "Honduras (le)", "HN", "HND", "340"},
|
||||
{"Hong Kong", "Hong Kong", "HK", "HKG", "344"},
|
||||
{"Hungary", "Hongrie (la)", "HU", "HUN", "348"},
|
||||
{"Iceland", "Islande (l')", "IS", "ISL", "352"},
|
||||
{"India", "Inde (l')", "IN", "IND", "356"},
|
||||
{"Indonesia", "Indonésie (l')", "ID", "IDN", "360"},
|
||||
{"Iran (Islamic Republic of)", "Iran (République Islamique d')", "IR", "IRN", "364"},
|
||||
{"Iraq", "Iraq (l')", "IQ", "IRQ", "368"},
|
||||
{"Ireland", "Irlande (l')", "IE", "IRL", "372"},
|
||||
{"Israel", "Israël", "IL", "ISR", "376"},
|
||||
{"Italy", "Italie (l')", "IT", "ITA", "380"},
|
||||
{"Côte d'Ivoire", "Côte d'Ivoire (la)", "CI", "CIV", "384"},
|
||||
{"Jamaica", "Jamaïque (la)", "JM", "JAM", "388"},
|
||||
{"Japan", "Japon (le)", "JP", "JPN", "392"},
|
||||
{"Kazakhstan", "Kazakhstan (le)", "KZ", "KAZ", "398"},
|
||||
{"Jordan", "Jordanie (la)", "JO", "JOR", "400"},
|
||||
{"Kenya", "Kenya (le)", "KE", "KEN", "404"},
|
||||
{"Korea (the Democratic People's Republic of)", "Corée (la République populaire démocratique de)", "KP", "PRK", "408"},
|
||||
{"Korea (the Republic of)", "Corée (la République de)", "KR", "KOR", "410"},
|
||||
{"Kuwait", "Koweït (le)", "KW", "KWT", "414"},
|
||||
{"Kyrgyzstan", "Kirghizistan (le)", "KG", "KGZ", "417"},
|
||||
{"Lao People's Democratic Republic (the)", "Lao, République démocratique populaire", "LA", "LAO", "418"},
|
||||
{"Lebanon", "Liban (le)", "LB", "LBN", "422"},
|
||||
{"Lesotho", "Lesotho (le)", "LS", "LSO", "426"},
|
||||
{"Latvia", "Lettonie (la)", "LV", "LVA", "428"},
|
||||
{"Liberia", "Libéria (le)", "LR", "LBR", "430"},
|
||||
{"Libya", "Libye (la)", "LY", "LBY", "434"},
|
||||
{"Liechtenstein", "Liechtenstein (le)", "LI", "LIE", "438"},
|
||||
{"Lithuania", "Lituanie (la)", "LT", "LTU", "440"},
|
||||
{"Luxembourg", "Luxembourg (le)", "LU", "LUX", "442"},
|
||||
{"Macao", "Macao", "MO", "MAC", "446"},
|
||||
{"Madagascar", "Madagascar", "MG", "MDG", "450"},
|
||||
{"Malawi", "Malawi (le)", "MW", "MWI", "454"},
|
||||
{"Malaysia", "Malaisie (la)", "MY", "MYS", "458"},
|
||||
{"Maldives", "Maldives (les)", "MV", "MDV", "462"},
|
||||
{"Mali", "Mali (le)", "ML", "MLI", "466"},
|
||||
{"Malta", "Malte", "MT", "MLT", "470"},
|
||||
{"Martinique", "Martinique (la)", "MQ", "MTQ", "474"},
|
||||
{"Mauritania", "Mauritanie (la)", "MR", "MRT", "478"},
|
||||
{"Mauritius", "Maurice", "MU", "MUS", "480"},
|
||||
{"Mexico", "Mexique (le)", "MX", "MEX", "484"},
|
||||
{"Monaco", "Monaco", "MC", "MCO", "492"},
|
||||
{"Mongolia", "Mongolie (la)", "MN", "MNG", "496"},
|
||||
{"Moldova (the Republic of)", "Moldova , République de", "MD", "MDA", "498"},
|
||||
{"Montenegro", "Monténégro (le)", "ME", "MNE", "499"},
|
||||
{"Montserrat", "Montserrat", "MS", "MSR", "500"},
|
||||
{"Morocco", "Maroc (le)", "MA", "MAR", "504"},
|
||||
{"Mozambique", "Mozambique (le)", "MZ", "MOZ", "508"},
|
||||
{"Oman", "Oman", "OM", "OMN", "512"},
|
||||
{"Namibia", "Namibie (la)", "NA", "NAM", "516"},
|
||||
{"Nauru", "Nauru", "NR", "NRU", "520"},
|
||||
{"Nepal", "Népal (le)", "NP", "NPL", "524"},
|
||||
{"Netherlands (the)", "Pays-Bas (les)", "NL", "NLD", "528"},
|
||||
{"Curaçao", "Curaçao", "CW", "CUW", "531"},
|
||||
{"Aruba", "Aruba", "AW", "ABW", "533"},
|
||||
{"Sint Maarten (Dutch part)", "Saint-Martin (partie néerlandaise)", "SX", "SXM", "534"},
|
||||
{"Bonaire, Sint Eustatius and Saba", "Bonaire, Saint-Eustache et Saba", "BQ", "BES", "535"},
|
||||
{"New Caledonia", "Nouvelle-Calédonie (la)", "NC", "NCL", "540"},
|
||||
{"Vanuatu", "Vanuatu (le)", "VU", "VUT", "548"},
|
||||
{"New Zealand", "Nouvelle-Zélande (la)", "NZ", "NZL", "554"},
|
||||
{"Nicaragua", "Nicaragua (le)", "NI", "NIC", "558"},
|
||||
{"Niger (the)", "Niger (le)", "NE", "NER", "562"},
|
||||
{"Nigeria", "Nigéria (le)", "NG", "NGA", "566"},
|
||||
{"Niue", "Niue", "NU", "NIU", "570"},
|
||||
{"Norfolk Island", "Norfolk (l'Île)", "NF", "NFK", "574"},
|
||||
{"Norway", "Norvège (la)", "NO", "NOR", "578"},
|
||||
{"Northern Mariana Islands (the)", "Mariannes du Nord (les Îles)", "MP", "MNP", "580"},
|
||||
{"United States Minor Outlying Islands (the)", "Îles mineures éloignées des États-Unis (les)", "UM", "UMI", "581"},
|
||||
{"Micronesia (Federated States of)", "Micronésie (États fédérés de)", "FM", "FSM", "583"},
|
||||
{"Marshall Islands (the)", "Marshall (Îles)", "MH", "MHL", "584"},
|
||||
{"Palau", "Palaos (les)", "PW", "PLW", "585"},
|
||||
{"Pakistan", "Pakistan (le)", "PK", "PAK", "586"},
|
||||
{"Panama", "Panama (le)", "PA", "PAN", "591"},
|
||||
{"Papua New Guinea", "Papouasie-Nouvelle-Guinée (la)", "PG", "PNG", "598"},
|
||||
{"Paraguay", "Paraguay (le)", "PY", "PRY", "600"},
|
||||
{"Peru", "Pérou (le)", "PE", "PER", "604"},
|
||||
{"Philippines (the)", "Philippines (les)", "PH", "PHL", "608"},
|
||||
{"Pitcairn", "Pitcairn", "PN", "PCN", "612"},
|
||||
{"Poland", "Pologne (la)", "PL", "POL", "616"},
|
||||
{"Portugal", "Portugal (le)", "PT", "PRT", "620"},
|
||||
{"Guinea-Bissau", "Guinée-Bissau (la)", "GW", "GNB", "624"},
|
||||
{"Timor-Leste", "Timor-Leste (le)", "TL", "TLS", "626"},
|
||||
{"Puerto Rico", "Porto Rico", "PR", "PRI", "630"},
|
||||
{"Qatar", "Qatar (le)", "QA", "QAT", "634"},
|
||||
{"Réunion", "Réunion (La)", "RE", "REU", "638"},
|
||||
{"Romania", "Roumanie (la)", "RO", "ROU", "642"},
|
||||
{"Russian Federation (the)", "Russie (la Fédération de)", "RU", "RUS", "643"},
|
||||
{"Rwanda", "Rwanda (le)", "RW", "RWA", "646"},
|
||||
{"Saint Barthélemy", "Saint-Barthélemy", "BL", "BLM", "652"},
|
||||
{"Saint Helena, Ascension and Tristan da Cunha", "Sainte-Hélène, Ascension et Tristan da Cunha", "SH", "SHN", "654"},
|
||||
{"Saint Kitts and Nevis", "Saint-Kitts-et-Nevis", "KN", "KNA", "659"},
|
||||
{"Anguilla", "Anguilla", "AI", "AIA", "660"},
|
||||
{"Saint Lucia", "Sainte-Lucie", "LC", "LCA", "662"},
|
||||
{"Saint Martin (French part)", "Saint-Martin (partie française)", "MF", "MAF", "663"},
|
||||
{"Saint Pierre and Miquelon", "Saint-Pierre-et-Miquelon", "PM", "SPM", "666"},
|
||||
{"Saint Vincent and the Grenadines", "Saint-Vincent-et-les Grenadines", "VC", "VCT", "670"},
|
||||
{"San Marino", "Saint-Marin", "SM", "SMR", "674"},
|
||||
{"Sao Tome and Principe", "Sao Tomé-et-Principe", "ST", "STP", "678"},
|
||||
{"Saudi Arabia", "Arabie saoudite (l')", "SA", "SAU", "682"},
|
||||
{"Senegal", "Sénégal (le)", "SN", "SEN", "686"},
|
||||
{"Serbia", "Serbie (la)", "RS", "SRB", "688"},
|
||||
{"Seychelles", "Seychelles (les)", "SC", "SYC", "690"},
|
||||
{"Sierra Leone", "Sierra Leone (la)", "SL", "SLE", "694"},
|
||||
{"Singapore", "Singapour", "SG", "SGP", "702"},
|
||||
{"Slovakia", "Slovaquie (la)", "SK", "SVK", "703"},
|
||||
{"Viet Nam", "Viet Nam (le)", "VN", "VNM", "704"},
|
||||
{"Slovenia", "Slovénie (la)", "SI", "SVN", "705"},
|
||||
{"Somalia", "Somalie (la)", "SO", "SOM", "706"},
|
||||
{"South Africa", "Afrique du Sud (l')", "ZA", "ZAF", "710"},
|
||||
{"Zimbabwe", "Zimbabwe (le)", "ZW", "ZWE", "716"},
|
||||
{"Spain", "Espagne (l')", "ES", "ESP", "724"},
|
||||
{"South Sudan", "Soudan du Sud (le)", "SS", "SSD", "728"},
|
||||
{"Sudan (the)", "Soudan (le)", "SD", "SDN", "729"},
|
||||
{"Western Sahara*", "Sahara occidental (le)*", "EH", "ESH", "732"},
|
||||
{"Suriname", "Suriname (le)", "SR", "SUR", "740"},
|
||||
{"Svalbard and Jan Mayen", "Svalbard et l'Île Jan Mayen (le)", "SJ", "SJM", "744"},
|
||||
{"Swaziland", "Swaziland (le)", "SZ", "SWZ", "748"},
|
||||
{"Sweden", "Suède (la)", "SE", "SWE", "752"},
|
||||
{"Switzerland", "Suisse (la)", "CH", "CHE", "756"},
|
||||
{"Syrian Arab Republic", "République arabe syrienne (la)", "SY", "SYR", "760"},
|
||||
{"Tajikistan", "Tadjikistan (le)", "TJ", "TJK", "762"},
|
||||
{"Thailand", "Thaïlande (la)", "TH", "THA", "764"},
|
||||
{"Togo", "Togo (le)", "TG", "TGO", "768"},
|
||||
{"Tokelau", "Tokelau (les)", "TK", "TKL", "772"},
|
||||
{"Tonga", "Tonga (les)", "TO", "TON", "776"},
|
||||
{"Trinidad and Tobago", "Trinité-et-Tobago (la)", "TT", "TTO", "780"},
|
||||
{"United Arab Emirates (the)", "Émirats arabes unis (les)", "AE", "ARE", "784"},
|
||||
{"Tunisia", "Tunisie (la)", "TN", "TUN", "788"},
|
||||
{"Turkey", "Turquie (la)", "TR", "TUR", "792"},
|
||||
{"Turkmenistan", "Turkménistan (le)", "TM", "TKM", "795"},
|
||||
{"Turks and Caicos Islands (the)", "Turks-et-Caïcos (les Îles)", "TC", "TCA", "796"},
|
||||
{"Tuvalu", "Tuvalu (les)", "TV", "TUV", "798"},
|
||||
{"Uganda", "Ouganda (l')", "UG", "UGA", "800"},
|
||||
{"Ukraine", "Ukraine (l')", "UA", "UKR", "804"},
|
||||
{"Macedonia (the former Yugoslav Republic of)", "Macédoine (l'ex‑République yougoslave de)", "MK", "MKD", "807"},
|
||||
{"Egypt", "Égypte (l')", "EG", "EGY", "818"},
|
||||
{"United Kingdom of Great Britain and Northern Ireland (the)", "Royaume-Uni de Grande-Bretagne et d'Irlande du Nord (le)", "GB", "GBR", "826"},
|
||||
{"Guernsey", "Guernesey", "GG", "GGY", "831"},
|
||||
{"Jersey", "Jersey", "JE", "JEY", "832"},
|
||||
{"Isle of Man", "Île de Man", "IM", "IMN", "833"},
|
||||
{"Tanzania, United Republic of", "Tanzanie, République-Unie de", "TZ", "TZA", "834"},
|
||||
{"United States of America (the)", "États-Unis d'Amérique (les)", "US", "USA", "840"},
|
||||
{"Virgin Islands (U.S.)", "Vierges des États-Unis (les Îles)", "VI", "VIR", "850"},
|
||||
{"Burkina Faso", "Burkina Faso (le)", "BF", "BFA", "854"},
|
||||
{"Uruguay", "Uruguay (l')", "UY", "URY", "858"},
|
||||
{"Uzbekistan", "Ouzbékistan (l')", "UZ", "UZB", "860"},
|
||||
{"Venezuela (Bolivarian Republic of)", "Venezuela (République bolivarienne du)", "VE", "VEN", "862"},
|
||||
{"Wallis and Futuna", "Wallis-et-Futuna", "WF", "WLF", "876"},
|
||||
{"Samoa", "Samoa (le)", "WS", "WSM", "882"},
|
||||
{"Yemen", "Yémen (le)", "YE", "YEM", "887"},
|
||||
{"Zambia", "Zambie (la)", "ZM", "ZMB", "894"},
|
||||
}
|
||||
268
vendor/github.com/asaskevich/govalidator/utils.go
generated
vendored
@@ -1,268 +0,0 @@
|
||||
package govalidator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"math"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Contains check if the string contains the substring.
|
||||
func Contains(str, substring string) bool {
|
||||
return strings.Contains(str, substring)
|
||||
}
|
||||
|
||||
// Matches check if string matches the pattern (pattern is regular expression)
|
||||
// In case of error return false
|
||||
func Matches(str, pattern string) bool {
|
||||
match, _ := regexp.MatchString(pattern, str)
|
||||
return match
|
||||
}
|
||||
|
||||
// LeftTrim trim characters from the left-side of the input.
|
||||
// If second argument is empty, it's will be remove leading spaces.
|
||||
func LeftTrim(str, chars string) string {
|
||||
pattern := ""
|
||||
if chars == "" {
|
||||
pattern = "^\\s+"
|
||||
} else {
|
||||
pattern = "^[" + chars + "]+"
|
||||
}
|
||||
r, _ := regexp.Compile(pattern)
|
||||
return string(r.ReplaceAll([]byte(str), []byte("")))
|
||||
}
|
||||
|
||||
// RightTrim trim characters from the right-side of the input.
|
||||
// If second argument is empty, it's will be remove spaces.
|
||||
func RightTrim(str, chars string) string {
|
||||
pattern := ""
|
||||
if chars == "" {
|
||||
pattern = "\\s+$"
|
||||
} else {
|
||||
pattern = "[" + chars + "]+$"
|
||||
}
|
||||
r, _ := regexp.Compile(pattern)
|
||||
return string(r.ReplaceAll([]byte(str), []byte("")))
|
||||
}
|
||||
|
||||
// Trim trim characters from both sides of the input.
|
||||
// If second argument is empty, it's will be remove spaces.
|
||||
func Trim(str, chars string) string {
|
||||
return LeftTrim(RightTrim(str, chars), chars)
|
||||
}
|
||||
|
||||
// WhiteList remove characters that do not appear in the whitelist.
|
||||
func WhiteList(str, chars string) string {
|
||||
pattern := "[^" + chars + "]+"
|
||||
r, _ := regexp.Compile(pattern)
|
||||
return string(r.ReplaceAll([]byte(str), []byte("")))
|
||||
}
|
||||
|
||||
// BlackList remove characters that appear in the blacklist.
|
||||
func BlackList(str, chars string) string {
|
||||
pattern := "[" + chars + "]+"
|
||||
r, _ := regexp.Compile(pattern)
|
||||
return string(r.ReplaceAll([]byte(str), []byte("")))
|
||||
}
|
||||
|
||||
// StripLow remove characters with a numerical value < 32 and 127, mostly control characters.
|
||||
// If keep_new_lines is true, newline characters are preserved (\n and \r, hex 0xA and 0xD).
|
||||
func StripLow(str string, keepNewLines bool) string {
|
||||
chars := ""
|
||||
if keepNewLines {
|
||||
chars = "\x00-\x09\x0B\x0C\x0E-\x1F\x7F"
|
||||
} else {
|
||||
chars = "\x00-\x1F\x7F"
|
||||
}
|
||||
return BlackList(str, chars)
|
||||
}
|
||||
|
||||
// ReplacePattern replace regular expression pattern in string
|
||||
func ReplacePattern(str, pattern, replace string) string {
|
||||
r, _ := regexp.Compile(pattern)
|
||||
return string(r.ReplaceAll([]byte(str), []byte(replace)))
|
||||
}
|
||||
|
||||
// Escape replace <, >, & and " with HTML entities.
|
||||
var Escape = html.EscapeString
|
||||
|
||||
func addSegment(inrune, segment []rune) []rune {
|
||||
if len(segment) == 0 {
|
||||
return inrune
|
||||
}
|
||||
if len(inrune) != 0 {
|
||||
inrune = append(inrune, '_')
|
||||
}
|
||||
inrune = append(inrune, segment...)
|
||||
return inrune
|
||||
}
|
||||
|
||||
// UnderscoreToCamelCase converts from underscore separated form to camel case form.
|
||||
// Ex.: my_func => MyFunc
|
||||
func UnderscoreToCamelCase(s string) string {
|
||||
return strings.Replace(strings.Title(strings.Replace(strings.ToLower(s), "_", " ", -1)), " ", "", -1)
|
||||
}
|
||||
|
||||
// CamelCaseToUnderscore converts from camel case form to underscore separated form.
|
||||
// Ex.: MyFunc => my_func
|
||||
func CamelCaseToUnderscore(str string) string {
|
||||
var output []rune
|
||||
var segment []rune
|
||||
for _, r := range str {
|
||||
if !unicode.IsLower(r) {
|
||||
output = addSegment(output, segment)
|
||||
segment = nil
|
||||
}
|
||||
segment = append(segment, unicode.ToLower(r))
|
||||
}
|
||||
output = addSegment(output, segment)
|
||||
return string(output)
|
||||
}
|
||||
|
||||
// Reverse return reversed string
|
||||
func Reverse(s string) string {
|
||||
r := []rune(s)
|
||||
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
|
||||
r[i], r[j] = r[j], r[i]
|
||||
}
|
||||
return string(r)
|
||||
}
|
||||
|
||||
// GetLines split string by "\n" and return array of lines
|
||||
func GetLines(s string) []string {
|
||||
return strings.Split(s, "\n")
|
||||
}
|
||||
|
||||
// GetLine return specified line of multiline string
|
||||
func GetLine(s string, index int) (string, error) {
|
||||
lines := GetLines(s)
|
||||
if index < 0 || index >= len(lines) {
|
||||
return "", errors.New("line index out of bounds")
|
||||
}
|
||||
return lines[index], nil
|
||||
}
|
||||
|
||||
// RemoveTags remove all tags from HTML string
|
||||
func RemoveTags(s string) string {
|
||||
return ReplacePattern(s, "<[^>]*>", "")
|
||||
}
|
||||
|
||||
// SafeFileName return safe string that can be used in file names
|
||||
func SafeFileName(str string) string {
|
||||
name := strings.ToLower(str)
|
||||
name = path.Clean(path.Base(name))
|
||||
name = strings.Trim(name, " ")
|
||||
separators, err := regexp.Compile(`[ &_=+:]`)
|
||||
if err == nil {
|
||||
name = separators.ReplaceAllString(name, "-")
|
||||
}
|
||||
legal, err := regexp.Compile(`[^[:alnum:]-.]`)
|
||||
if err == nil {
|
||||
name = legal.ReplaceAllString(name, "")
|
||||
}
|
||||
for strings.Contains(name, "--") {
|
||||
name = strings.Replace(name, "--", "-", -1)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// NormalizeEmail canonicalize an email address.
|
||||
// The local part of the email address is lowercased for all domains; the hostname is always lowercased and
|
||||
// the local part of the email address is always lowercased for hosts that are known to be case-insensitive (currently only GMail).
|
||||
// Normalization follows special rules for known providers: currently, GMail addresses have dots removed in the local part and
|
||||
// are stripped of tags (e.g. some.one+tag@gmail.com becomes someone@gmail.com) and all @googlemail.com addresses are
|
||||
// normalized to @gmail.com.
|
||||
func NormalizeEmail(str string) (string, error) {
|
||||
if !IsEmail(str) {
|
||||
return "", fmt.Errorf("%s is not an email", str)
|
||||
}
|
||||
parts := strings.Split(str, "@")
|
||||
parts[0] = strings.ToLower(parts[0])
|
||||
parts[1] = strings.ToLower(parts[1])
|
||||
if parts[1] == "gmail.com" || parts[1] == "googlemail.com" {
|
||||
parts[1] = "gmail.com"
|
||||
parts[0] = strings.Split(ReplacePattern(parts[0], `\.`, ""), "+")[0]
|
||||
}
|
||||
return strings.Join(parts, "@"), nil
|
||||
}
|
||||
|
||||
// Truncate a string to the closest length without breaking words.
|
||||
func Truncate(str string, length int, ending string) string {
|
||||
var aftstr, befstr string
|
||||
if len(str) > length {
|
||||
words := strings.Fields(str)
|
||||
before, present := 0, 0
|
||||
for i := range words {
|
||||
befstr = aftstr
|
||||
before = present
|
||||
aftstr = aftstr + words[i] + " "
|
||||
present = len(aftstr)
|
||||
if present > length && i != 0 {
|
||||
if (length - before) < (present - length) {
|
||||
return Trim(befstr, " /\\.,\"'#!?&@+-") + ending
|
||||
}
|
||||
return Trim(aftstr, " /\\.,\"'#!?&@+-") + ending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// Pad left side of string if size of string is less then indicated pad length
|
||||
func PadLeft(str string, padStr string, padLen int) string {
|
||||
return buildPadStr(str, padStr, padLen, true, false)
|
||||
}
|
||||
|
||||
// Pad right side of string if size of string is less then indicated pad length
|
||||
func PadRight(str string, padStr string, padLen int) string {
|
||||
return buildPadStr(str, padStr, padLen, false, true)
|
||||
}
|
||||
|
||||
// Pad both sides of string if size of string is less then indicated pad length
|
||||
func PadBoth(str string, padStr string, padLen int) string {
|
||||
return buildPadStr(str, padStr, padLen, true, true)
|
||||
}
|
||||
|
||||
// Pad string either left, right or both sides, not the padding string can be unicode and more then one
|
||||
// character
|
||||
func buildPadStr(str string, padStr string, padLen int, padLeft bool, padRight bool) string {
|
||||
|
||||
// When padded length is less then the current string size
|
||||
if padLen < utf8.RuneCountInString(str) {
|
||||
return str
|
||||
}
|
||||
|
||||
padLen -= utf8.RuneCountInString(str)
|
||||
|
||||
targetLen := padLen
|
||||
|
||||
targetLenLeft := targetLen
|
||||
targetLenRight := targetLen
|
||||
if padLeft && padRight {
|
||||
targetLenLeft = padLen / 2
|
||||
targetLenRight = padLen - targetLenLeft
|
||||
}
|
||||
|
||||
strToRepeatLen := utf8.RuneCountInString(padStr)
|
||||
|
||||
repeatTimes := int(math.Ceil(float64(targetLen) / float64(strToRepeatLen)))
|
||||
repeatedString := strings.Repeat(padStr, repeatTimes)
|
||||
|
||||
leftSide := ""
|
||||
if padLeft {
|
||||
leftSide = repeatedString[0:targetLenLeft]
|
||||
}
|
||||
|
||||
rightSide := ""
|
||||
if padRight {
|
||||
rightSide = repeatedString[0:targetLenRight]
|
||||
}
|
||||
|
||||
return leftSide + str + rightSide
|
||||
}
|
||||
1022
vendor/github.com/asaskevich/govalidator/validator.go
generated
vendored
15
vendor/github.com/asaskevich/govalidator/wercker.yml
generated
vendored
@@ -1,15 +0,0 @@
|
||||
box: golang
|
||||
build:
|
||||
steps:
|
||||
- setup-go-workspace
|
||||
|
||||
- script:
|
||||
name: go get
|
||||
code: |
|
||||
go version
|
||||
go get -t ./...
|
||||
|
||||
- script:
|
||||
name: go test
|
||||
code: |
|
||||
go test -race ./...
|
||||
21
vendor/github.com/dustin/go-humanize/LICENSE
generated
vendored
@@ -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>
|
||||
92
vendor/github.com/dustin/go-humanize/README.markdown
generated
vendored
@@ -1,92 +0,0 @@
|
||||
# Humane Units [](https://travis-ci.org/dustin/go-humanize) [](https://godoc.org/github.com/dustin/go-humanize)
|
||||
|
||||
Just a few functions for helping humanize times and sizes.
|
||||
|
||||
`go get` it as `github.com/dustin/go-humanize`, import it as
|
||||
`"github.com/dustin/go-humanize"`, use it as `humanize`
|
||||
|
||||
See [godoc](https://godoc.org/github.com/dustin/go-humanize) for
|
||||
complete documentation.
|
||||
|
||||
## Sizes
|
||||
|
||||
This lets you take numbers like `82854982` and convert them to useful
|
||||
strings like, `83MB` or `79MiB` (whichever you prefer).
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("That file is %s.", humanize.Bytes(82854982))
|
||||
```
|
||||
|
||||
## Times
|
||||
|
||||
This lets you take a `time.Time` and spit it out in relative terms.
|
||||
For example, `12 seconds ago` or `3 days from now`.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("This was touched %s", humanize.Time(someTimeInstance))
|
||||
```
|
||||
|
||||
Thanks to Kyle Lemons for the time implementation from an IRC
|
||||
conversation one day. It's pretty neat.
|
||||
|
||||
## Ordinals
|
||||
|
||||
From a [mailing list discussion][odisc] where a user wanted to be able
|
||||
to label ordinals.
|
||||
|
||||
0 -> 0th
|
||||
1 -> 1st
|
||||
2 -> 2nd
|
||||
3 -> 3rd
|
||||
4 -> 4th
|
||||
[...]
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193))
|
||||
```
|
||||
|
||||
## Commas
|
||||
|
||||
Want to shove commas into numbers? Be my guest.
|
||||
|
||||
0 -> 0
|
||||
100 -> 100
|
||||
1000 -> 1,000
|
||||
1000000000 -> 1,000,000,000
|
||||
-100000 -> -100,000
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491))
|
||||
```
|
||||
|
||||
## Ftoa
|
||||
|
||||
Nicer float64 formatter that removes trailing zeros.
|
||||
|
||||
```go
|
||||
fmt.Printf("%f", 2.24) // 2.240000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
|
||||
fmt.Printf("%f", 2.0) // 2.000000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
|
||||
```
|
||||
|
||||
## SI notation
|
||||
|
||||
Format numbers with [SI notation][sinotation].
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
humanize.SI(0.00000000223, "M") // 2.23nM
|
||||
```
|
||||
|
||||
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
|
||||
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix
|
||||
31
vendor/github.com/dustin/go-humanize/big.go
generated
vendored
@@ -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
|
||||
}
|
||||
173
vendor/github.com/dustin/go-humanize/bigbytes.go
generated
vendored
@@ -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)
|
||||
}
|
||||
143
vendor/github.com/dustin/go-humanize/bytes.go
generated
vendored
@@ -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)
|
||||
}
|
||||
108
vendor/github.com/dustin/go-humanize/comma.go
generated
vendored
@@ -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:], ",")
|
||||
}
|
||||
40
vendor/github.com/dustin/go-humanize/commaf.go
generated
vendored
@@ -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()
|
||||
}
|
||||
23
vendor/github.com/dustin/go-humanize/ftoa.go
generated
vendored
@@ -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))
|
||||
}
|
||||
8
vendor/github.com/dustin/go-humanize/humanize.go
generated
vendored
@@ -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
|
||||
192
vendor/github.com/dustin/go-humanize/number.go
generated
vendored
@@ -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###,##" => "12 345,68"
|
||||
// "#.###,###### => 12.345,678900
|
||||
// "" (aka default format) => 12,345.67
|
||||
//
|
||||
// The highest precision allowed is 9 digits after the decimal symbol.
|
||||
// There is also a version for integer number, FormatInteger(),
|
||||
// which is convenient for calls within template.
|
||||
func FormatFloat(format string, n float64) string {
|
||||
// Special cases:
|
||||
// NaN = "NaN"
|
||||
// +Inf = "+Infinity"
|
||||
// -Inf = "-Infinity"
|
||||
if math.IsNaN(n) {
|
||||
return "NaN"
|
||||
}
|
||||
if n > math.MaxFloat64 {
|
||||
return "Infinity"
|
||||
}
|
||||
if n < -math.MaxFloat64 {
|
||||
return "-Infinity"
|
||||
}
|
||||
|
||||
// default format
|
||||
precision := 2
|
||||
decimalStr := "."
|
||||
thousandStr := ","
|
||||
positiveStr := ""
|
||||
negativeStr := "-"
|
||||
|
||||
if len(format) > 0 {
|
||||
format := []rune(format)
|
||||
|
||||
// If there is an explicit format directive,
|
||||
// then default values are these:
|
||||
precision = 9
|
||||
thousandStr = ""
|
||||
|
||||
// collect indices of meaningful formatting directives
|
||||
formatIndx := []int{}
|
||||
for i, char := range format {
|
||||
if char != '#' && char != '0' {
|
||||
formatIndx = append(formatIndx, i)
|
||||
}
|
||||
}
|
||||
|
||||
if len(formatIndx) > 0 {
|
||||
// Directive at index 0:
|
||||
// Must be a '+'
|
||||
// Raise an error if not the case
|
||||
// index: 0123456789
|
||||
// +0.000,000
|
||||
// +000,000.0
|
||||
// +0000.00
|
||||
// +0000
|
||||
if formatIndx[0] == 0 {
|
||||
if format[formatIndx[0]] != '+' {
|
||||
panic("RenderFloat(): invalid positive sign directive")
|
||||
}
|
||||
positiveStr = "+"
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// Two directives:
|
||||
// First is thousands separator
|
||||
// Raise an error if not followed by 3-digit
|
||||
// 0123456789
|
||||
// 0.000,000
|
||||
// 000,000.00
|
||||
if len(formatIndx) == 2 {
|
||||
if (formatIndx[1] - formatIndx[0]) != 4 {
|
||||
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
|
||||
}
|
||||
thousandStr = string(format[formatIndx[0]])
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// One directive:
|
||||
// Directive is decimal separator
|
||||
// The number of digit-specifier following the separator indicates wanted precision
|
||||
// 0123456789
|
||||
// 0.00
|
||||
// 000,0000
|
||||
if len(formatIndx) == 1 {
|
||||
decimalStr = string(format[formatIndx[0]])
|
||||
precision = len(format) - formatIndx[0] - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate sign part
|
||||
var signStr string
|
||||
if n >= 0.000000001 {
|
||||
signStr = positiveStr
|
||||
} else if n <= -0.000000001 {
|
||||
signStr = negativeStr
|
||||
n = -n
|
||||
} else {
|
||||
signStr = ""
|
||||
n = 0.0
|
||||
}
|
||||
|
||||
// split number into integer and fractional parts
|
||||
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
|
||||
|
||||
// generate integer part string
|
||||
intStr := strconv.FormatInt(int64(intf), 10)
|
||||
|
||||
// add thousand separator if required
|
||||
if len(thousandStr) > 0 {
|
||||
for i := len(intStr); i > 3; {
|
||||
i -= 3
|
||||
intStr = intStr[:i] + thousandStr + intStr[i:]
|
||||
}
|
||||
}
|
||||
|
||||
// no fractional part, we can leave now
|
||||
if precision == 0 {
|
||||
return signStr + intStr
|
||||
}
|
||||
|
||||
// generate fractional part
|
||||
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
|
||||
// may need padding
|
||||
if len(fracStr) < precision {
|
||||
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
|
||||
}
|
||||
|
||||
return signStr + intStr + decimalStr + fracStr
|
||||
}
|
||||
|
||||
// FormatInteger produces a formatted number as string.
|
||||
// See FormatFloat.
|
||||
func FormatInteger(format string, n int) string {
|
||||
return FormatFloat(format, float64(n))
|
||||
}
|
||||
25
vendor/github.com/dustin/go-humanize/ordinals.go
generated
vendored
@@ -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
|
||||
}
|
||||
113
vendor/github.com/dustin/go-humanize/si.go
generated
vendored
@@ -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
|
||||
}
|
||||
117
vendor/github.com/dustin/go-humanize/times.go
generated
vendored
@@ -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...)
|
||||
}
|
||||
27
vendor/github.com/gliderlabs/ssh/LICENSE
generated
vendored
@@ -1,27 +0,0 @@
|
||||
Copyright (c) 2016 Glider Labs. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Glider Labs nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
96
vendor/github.com/gliderlabs/ssh/README.md
generated
vendored
@@ -1,96 +0,0 @@
|
||||
# gliderlabs/ssh
|
||||
|
||||
[](https://godoc.org/github.com/gliderlabs/ssh)
|
||||
[](https://circleci.com/gh/gliderlabs/ssh)
|
||||
[](https://goreportcard.com/report/github.com/gliderlabs/ssh)
|
||||
[](#sponsors)
|
||||
[](http://slack.gliderlabs.com)
|
||||
[](https://app.convertkit.com/landing_pages/243312)
|
||||
|
||||
> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member
|
||||
|
||||
This Go package wraps the [crypto/ssh
|
||||
package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for
|
||||
building SSH servers. The goal of the API was to make it as simple as using
|
||||
[net/http](https://golang.org/pkg/net/http/), so the API is very similar:
|
||||
|
||||
```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
|
||||
83
vendor/github.com/gliderlabs/ssh/agent.go
generated
vendored
@@ -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)
|
||||
}
|
||||
}
|
||||
10
vendor/github.com/gliderlabs/ssh/circle.yml
generated
vendored
@@ -1,10 +0,0 @@
|
||||
version: 2.0
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: golang:1.8
|
||||
working_directory: /go/src/github.com/gliderlabs/ssh
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get
|
||||
- run: go test -v -race
|
||||
55
vendor/github.com/gliderlabs/ssh/conn.go
generated
vendored
@@ -1,55 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type serverConn struct {
|
||||
net.Conn
|
||||
|
||||
idleTimeout time.Duration
|
||||
maxDeadline time.Time
|
||||
closeCanceler context.CancelFunc
|
||||
}
|
||||
|
||||
func (c *serverConn) Write(p []byte) (n int, err error) {
|
||||
c.updateDeadline()
|
||||
n, err = c.Conn.Write(p)
|
||||
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
|
||||
c.closeCanceler()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *serverConn) Read(b []byte) (n int, err error) {
|
||||
c.updateDeadline()
|
||||
n, err = c.Conn.Read(b)
|
||||
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
|
||||
c.closeCanceler()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *serverConn) Close() (err error) {
|
||||
err = c.Conn.Close()
|
||||
if c.closeCanceler != nil {
|
||||
c.closeCanceler()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *serverConn) updateDeadline() {
|
||||
switch {
|
||||
case c.idleTimeout > 0:
|
||||
idleDeadline := time.Now().Add(c.idleTimeout)
|
||||
if idleDeadline.Unix() < c.maxDeadline.Unix() {
|
||||
c.Conn.SetDeadline(idleDeadline)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
c.Conn.SetDeadline(c.maxDeadline)
|
||||
}
|
||||
}
|
||||
148
vendor/github.com/gliderlabs/ssh/context.go
generated
vendored
@@ -1,148 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// contextKey is a value for use with context.WithValue. It's used as
|
||||
// a pointer so it fits in an interface{} without allocation.
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var (
|
||||
// ContextKeyUser is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type string.
|
||||
ContextKeyUser = &contextKey{"user"}
|
||||
|
||||
// ContextKeySessionID is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type string.
|
||||
ContextKeySessionID = &contextKey{"session-id"}
|
||||
|
||||
// ContextKeyPermissions is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type *Permissions.
|
||||
ContextKeyPermissions = &contextKey{"permissions"}
|
||||
|
||||
// ContextKeyClientVersion is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type string.
|
||||
ContextKeyClientVersion = &contextKey{"client-version"}
|
||||
|
||||
// ContextKeyServerVersion is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type string.
|
||||
ContextKeyServerVersion = &contextKey{"server-version"}
|
||||
|
||||
// ContextKeyLocalAddr is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type net.Addr.
|
||||
ContextKeyLocalAddr = &contextKey{"local-addr"}
|
||||
|
||||
// ContextKeyRemoteAddr is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type net.Addr.
|
||||
ContextKeyRemoteAddr = &contextKey{"remote-addr"}
|
||||
|
||||
// ContextKeyServer is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type *Server.
|
||||
ContextKeyServer = &contextKey{"ssh-server"}
|
||||
|
||||
// ContextKeyConn is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type gossh.Conn.
|
||||
ContextKeyConn = &contextKey{"ssh-conn"}
|
||||
|
||||
// ContextKeyPublicKey is a context key for use with Contexts in this package.
|
||||
// The associated value will be of type PublicKey.
|
||||
ContextKeyPublicKey = &contextKey{"public-key"}
|
||||
)
|
||||
|
||||
// Context is a package specific context interface. It exposes connection
|
||||
// metadata and allows new values to be easily written to it. It's used in
|
||||
// authentication handlers and callbacks, and its underlying context.Context is
|
||||
// exposed on Session in the session Handler.
|
||||
type Context interface {
|
||||
context.Context
|
||||
|
||||
// User returns the username used when establishing the SSH connection.
|
||||
User() string
|
||||
|
||||
// SessionID returns the session hash.
|
||||
SessionID() string
|
||||
|
||||
// ClientVersion returns the version reported by the client.
|
||||
ClientVersion() string
|
||||
|
||||
// ServerVersion returns the version reported by the server.
|
||||
ServerVersion() string
|
||||
|
||||
// RemoteAddr returns the remote address for this connection.
|
||||
RemoteAddr() net.Addr
|
||||
|
||||
// LocalAddr returns the local address for this connection.
|
||||
LocalAddr() net.Addr
|
||||
|
||||
// Permissions returns the Permissions object used for this connection.
|
||||
Permissions() *Permissions
|
||||
|
||||
// SetValue allows you to easily write new values into the underlying context.
|
||||
SetValue(key, value interface{})
|
||||
}
|
||||
|
||||
type sshContext struct {
|
||||
context.Context
|
||||
}
|
||||
|
||||
func newContext(srv *Server) (*sshContext, context.CancelFunc) {
|
||||
innerCtx, cancel := context.WithCancel(context.Background())
|
||||
ctx := &sshContext{innerCtx}
|
||||
ctx.SetValue(ContextKeyServer, srv)
|
||||
perms := &Permissions{&gossh.Permissions{}}
|
||||
ctx.SetValue(ContextKeyPermissions, perms)
|
||||
return ctx, cancel
|
||||
}
|
||||
|
||||
// this is separate from newContext because we will get ConnMetadata
|
||||
// at different points so it needs to be applied separately
|
||||
func 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)
|
||||
}
|
||||
47
vendor/github.com/gliderlabs/ssh/doc.go
generated
vendored
@@ -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
|
||||
77
vendor/github.com/gliderlabs/ssh/options.go
generated
vendored
@@ -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
|
||||
}
|
||||
}
|
||||