Compare commits

...

41 Commits

Author SHA1 Message Date
Manfred Touron
554937dd7a v1.6.0 2017-12-12 10:39:36 +01:00
Manfred Touron
4aa9a227e8 Switch to a lonely tmux demo 2017-12-12 10:15:09 +01:00
Manfred Touron
6e4cbf5dd8 Add server/client gif example in README 2017-12-12 09:05:49 +01:00
Manfred Touron
44d1ac7f11 Add example gifs 2017-12-12 09:01:33 +01:00
Manfred Touron
1c32da7751 Merge pull request #12 from moul/dev/moul/key-show
Add 'key show KEY' command
2017-12-06 00:37:37 +01:00
Manfred Touron
999b740df6 Add 'key show KEY' command (#11) 2017-12-06 00:33:40 +01:00
Manfred Touron
6864b7ca10 govendor add github.com/mgutz/ansi + its deps 2017-12-06 00:33:40 +01:00
Manfred Touron
546b350a6c Create LICENSE 2017-12-04 16:45:42 +01:00
Manfred Touron
d70296cd95 Update README.md 2017-12-04 16:33:41 +01:00
Manfred Touron
10f4ad49d9 Improve logging 2017-12-04 11:14:20 +01:00
Manfred Touron
edb230b278 Update README.md 2017-12-04 10:41:25 +01:00
Manfred Touron
efbf66a0a4 Update README.md 2017-12-04 10:39:40 +01:00
Manfred Touron
0746458762 Merge pull request #10 from moul/dev/moul/circle
Dev/moul/circle
2017-12-04 10:37:24 +01:00
Manfred Touron
f2738e2bd1 Fix lint warns 2017-12-04 10:32:29 +01:00
Manfred Touron
b0d8180809 Add CircleCI config 2017-12-04 10:21:36 +01:00
Manfred Touron
f9d450ffaf Add healthcheck user 2017-12-04 09:34:52 +01:00
Manfred Touron
391a39d82c Add --latest and --quiet options to ls commands 2017-12-04 09:27:10 +01:00
Manfred Touron
7eb76c861f Lint code + fix tests 2017-12-03 18:18:17 +01:00
Manfred Touron
cd437a3a7b Post-release version bump 2017-12-02 01:12:29 +01:00
Manfred Touron
2accc7abd4 v1.5.0 2017-12-02 01:11:40 +01:00
Manfred Touron
3c10578584 Fix some backup/restore bugs + improve MySQL support 2017-12-02 00:01:31 +01:00
Manfred Touron
511470087b Host key checking shared across users 2017-12-01 22:19:22 +01:00
Manfred Touron
017ee2ab39 Add MySQL support 2017-11-29 14:07:59 +01:00
Manfred Touron
b093f61fb5 Switch to hard delete 2017-11-29 10:27:04 +01:00
Manfred Touron
bd158819d3 Add 'make dev EXTRA_RUN_OPTS' flag
make dev EXTRA_RUN_OPTS="--db-conn=root@/db?parseTime=true --db-driver=mysql"
2017-11-29 10:25:52 +01:00
Manfred Touron
86f6e87efe Add audit log 2017-11-29 09:17:19 +01:00
Manfred Touron
e377cac8e6 Ignore some errors when logging closed connections 2017-11-28 20:08:31 +01:00
Manfred Touron
0fbcc0dd41 Session management 2017-11-27 08:52:33 +01:00
Manfred Touron
1fdf37dc07 Create Session objects on each connections (history) 2017-11-27 08:22:13 +01:00
Manfred Touron
4cf73e3410 Moved demo code in the README as example 2017-11-27 08:09:22 +01:00
Manfred Touron
328bb0153b Add session model 2017-11-27 07:43:52 +01:00
Manfred Touron
1ddd6867b6 Post-release version bump 2017-11-24 15:22:50 +01:00
Manfred Touron
2becd5eec2 v1.4.0 2017-11-24 15:22:22 +01:00
Manfred Touron
571b37da6b Add option to encrypt sensitive data 2017-11-24 15:15:24 +01:00
Manfred Touron
01d464f4c5 Sort items by created_at in 'ls' commands 2017-11-24 07:27:38 +01:00
Manfred Touron
bf184c621d Merge branch 'dev/moul/timeago'
* dev/moul/timeago:
  Add Updated and Created fields in 'ls' commands
  govendor add github.com/dustin/go-humanize
2017-11-24 06:48:07 +01:00
Manfred Touron
f4309f843b Add Updated and Created fields in 'ls' commands 2017-11-24 06:47:39 +01:00
Manfred Touron
cbdc231cbf govendor add github.com/dustin/go-humanize 2017-11-24 06:46:54 +01:00
Manfred Touron
0f0a8dd9bb Add 'key setup' command (easy SSH key installation) 2017-11-24 05:04:22 +01:00
Manfred Touron
4189eb8154 Update README.md 2017-11-23 19:06:30 +01:00
Manfred Touron
1d6349767d Post-commit version bump 2017-11-23 19:04:57 +01:00
52 changed files with 4453 additions and 230 deletions

BIN
.assets/client.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

BIN
.assets/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
.assets/server.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

17
.circleci/config.yml Normal file
View File

@@ -0,0 +1,17 @@
version: 2
jobs:
build:
docker:
- image: circleci/golang:1.8
# - image: circleci/mysql:9.4
working_directory: /go/src/github.com/moul/sshportal
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

View File

@@ -1,5 +1,26 @@
# Changelog
## 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

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 Manfred Touron <m@42.am>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,13 +1,14 @@
GIT_SHA ?= $(shell git rev-parse HEAD)
GIT_TAG ?= $(shell git describe --tags --always)
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
LDFLAGS ?= -X main.GIT_SHA=$(GIT_SHA) -X main.GIT_TAG=$(GIT_TAG) -X main.GIT_BRANCH=$(GIT_BRANCH)
LDFLAGS ?= -X main.GitSha=$(GIT_SHA) -X main.GitTag=$(GIT_TAG) -X main.GitBranch=$(GIT_BRANCH)
VERSION ?= $(shell grep 'VERSION =' main.go | cut -d'"' -f2)
PORT ?= 2222
AES_KEY ?= my-dummy-aes-key
.PHONY: install
install:
go install -ldflags '$(LDFLAGS)' .
go install -v -ldflags '$(LDFLAGS)' .
.PHONY: docker.build
docker.build:
@@ -24,13 +25,17 @@ _docker_install:
.PHONY: dev
dev:
-go get github.com/githubnemo/CompileDaemon
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal --demo --debug --bind-address=:$(PORT)" .
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal --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=20s .
.PHONY: backup
backup:
mkdir -p data/backups

141
README.md
View File

@@ -1,7 +1,17 @@
# sshportal
[![CircleCI](https://circleci.com/gh/moul/sshportal.svg?style=svg)](https://circleci.com/gh/moul/sshportal)
[![Docker Build Status](https://img.shields.io/docker/build/moul/sshportal.svg)](https://hub.docker.com/r/moul/sshportal/)
[![GoDoc](https://godoc.org/github.com/moul/sshportal?status.svg)](https://godoc.org/github.com/moul/sshportal)
[![License](https://img.shields.io/github/license/moul/sshportal.svg)](https://github.com/moul/sshportal/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/release/moul/sshportal.svg)](https://github.com/moul/sshportal/releases)
Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
![sshportal demo](https://github.com/moul/sshportal/raw/master/.assets/demo.gif)
---
```
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
DMZ │
@@ -31,6 +41,14 @@ Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
* ACL management
* Connect to host using key or password
* Admin commands can be run directly or in an interactive shell
* User Roles
* User invitations
* Easy authorized_keys installation
* Sensitive data encryption
* Session management
* Audit log
* Host Keys verifications shared across users
* Healthcheck user
## Usage
@@ -46,7 +64,7 @@ Link your SSH key with the admin account
```console
$ ssh localhost -p 2222 -l invite:BpLnfgDsc2WD8F2q
Welcome Administrator!
Welcome admin!
Your key is now associated with the user "admin@sshportal".
Shared connection to localhost closed.
@@ -80,28 +98,18 @@ List hosts
```console
config> host ls
ID | NAME | URL | KEY | PASS | GROUPS | COMMENT
+----+------+-------------------------+---------+------+--------+---------+
1 | foo | bart@foo.example.org:22 | default | | 1 |
ID | NAME | URL | KEY | PASS | GROUPS | COMMENT
+----+------+-------------------------+---------+------+---------+---------+
1 | foo | bart@foo.example.org:22 | default | | default |
Total: 1 hosts.
config>
```
Get the default key in authorized_keys format
Add the key to the server
```console
config> key inspect default
[...]
"PubKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvUP/8FedyIe+a+RWU4KvJ1+iZwtWmY9czJubLwN4RcjKHQMzLqWC7pKZHAABCZjLJjVD/3Zb53jZwbh7mysAkocundMpvUL5+Yb4a8lDiflXkdXT9fZCx+ibJBk4jRnKLGIneSzVtFEerEwQKKnKQoCgPkZwCDaL/jHhDlOmAvxqAJrjiy42HXwppX2UuF8zujs6OKHRYJ/Q1vo0caa6/o1eoyXE9OrOwIk+IcAN3YIQi/B1BOlZOQBzHIZz83AFlD2TcPhyYcbxPyKGih84Zr3rQaaP1WiaiPqxzp3s5OhTLthc5XtCSLzmRSLvgC2eFdNhBDB5KLtO2khBkz5ID",
[...]
config>
```
Add this key to the server
```console
$ ssh bart@foo.example.org
> umask 077; mkdir -p .ssh; echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvUP/8FedyIe+a+RWU4KvJ1+iZwtWmY9czJubLwN4RcjKHQMzLqWC7pKZHAABCZjLJjVD/3Zb53jZwbh7mysAkocundMpvUL5+Yb4a8lDiflXkdXT9fZCx+ibJBk4jRnKLGIneSzVtFEerEwQKKnKQoCgPkZwCDaL/jHhDlOmAvxqAJrjiy42HXwppX2UuF8zujs6OKHRYJ/Q1vo0caa6/o1eoyXE9OrOwIk+IcAN3YIQi/B1BOlZOQBzHIZz83AFlD2TcPhyYcbxPyKGih84Zr3rQaaP1WiaiPqxzp3s5OhTLthc5XtCSLzmRSLvgC2eFdNhBDB5KLtO2khBkz5ID >> .ssh/authorized_keys
$ ssh bart@foo.example.org "$(ssh localhost -p 2222 -l admin key setup default)"
$
```
Profit
@@ -122,7 +130,7 @@ config>
## CLI
sshportal embeds a configuration CLI.
`sshportal` embeds a configuration CLI.
By default, the configuration user is `admin`, (can be changed using `--config-user=<value>` when starting the server.
@@ -141,42 +149,54 @@ You can enter in interactive mode using this syntax: `ssh admin@portal.example.o
acl help
acl create [-h] [--hostgroup=HOSTGROUP...] [--usergroup=USERGROUP...] [--pattern=<value>] [--comment=<value>] [--action=<value>] [--weight=value]
acl inspect [-h] ACL...
acl ls [-h]
acl ls [-h] [--latest] [--quiet]
acl rm [-h] ACL...
acl update [-h] [--comment=<value>] [--action=<value>] [--weight=<value>] [--assign-hostgroup=HOSTGROUP...] [--unassign-hostgroup=HOSTGROUP...] [--assign-usergroup=USERGROUP...] [--unassign-usergroup=USERGROUP...] ACL...
# config management
config help
config backup [-h] [--indent]
config restore [-h] [--confirm]
config backup [-h] [--indent] [--decrypt]
config restore [-h] [--confirm] [--decrypt]
# event management
event help
event ls [-h] [--latest] [--quiet]
event inspect [-h] EVENT...
# host management
host help
host create [-h] [--name=<value>] [--password=<value>] [--fingerprint=<value>] [--comment=<value>] [--key=KEY] [--group=HOSTGROUP...] <username>[:<password>]@<host>[:<port>]
host inspect [-h] HOST...
host ls [-h]
host create [-h] [--name=<value>] [--password=<value>] [--comment=<value>] [--key=KEY] [--group=HOSTGROUP...] <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>] [--fingerprint=<value>] [--key=KEY] [--assign-group=HOSTGROUP...] [--unassign-group=HOSTGROUP...] HOST...
host update [-h] [--name=<value>] [--comment=<value>] [--key=KEY] [--assign-group=HOSTGROUP...] [--unassign-group=HOSTGROUP...] HOST...
# hostgroup management
hostgroup help
hostgroup create [-h] [--name=<value>] [--comment=<value>]
hostgroup inspect [-h] HOSTGROUP...
hostgroup ls [-h]
hostgroup ls [-h] [--latest] [--quiet]
hostgroup rm [-h] HOSTGROUP...
# key management
key help
key create [-h] [--name=<value>] [--type=<value>] [--length=<value>] [--comment=<value>]
key inspect [-h] KEY...
key ls [-h]
key inspect [-h] [--decrypt] KEY...
key ls [-h] [--latest] [--quiet]
key rm [-h] KEY...
key setup [-h] KEY
key show [-h] KEY
# session management
session help
session ls [-h] [--latest] [--quiet]
session inspect [-h] SESSION...
# user management
user help
user invite [-h] [--name=<value>] [--comment=<value>] [--group=USERGROUP...] <email>
user inspect [-h] USER...
user ls [-h]
user ls [-h] [--latest] [--quiet]
user rm [-h] USER...
user update [-h] [--name=<value>] [--email=<value>] [--set-admin] [--unset-admin] [--assign-group=USERGROUP...] [--unassign-group=USERGROUP...] USER...
@@ -184,7 +204,7 @@ user update [-h] [--name=<value>] [--email=<value>] [--set-admin] [--unset-admin
usergroup help
hostgroup create [-h] [--name=<value>] [--comment=<value>]
usergroup inspect [-h] USERGROUP...
usergroup ls [-h]
usergroup ls [-h] [--latest] [--quiet]
usergroup rm [-h] USERGROUP...
# other
@@ -203,7 +223,7 @@ An [automated build is setup on the Docker Hub](https://hub.docker.com/r/moul/ss
```console
# Start a server in background
# mount `pwd` to persist the sqlite database file
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.3.0
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.6.0
# check logs (mandatory on first run to get the administrator invite token)
docker logs -f sshportal
@@ -212,7 +232,7 @@ docker logs -f sshportal
The easier way to upgrade sshportal is to do the following:
```sh
# we consider you were using the version v1.2.0 and you want to use the new version v1.3.0
# we consider you were using an old version and you want to use the new version v1.6.0
# stop and rename the last working container + backup the database
docker stop sshportal
@@ -220,7 +240,7 @@ docker rename sshportal sshportal_old
cp sshportal.db sshportal.db.bkp
# run the new version
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.3.0
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.6.0
# check the logs for migration or cross-version incompabitility errors
docker logs -f sshportal
```
@@ -246,16 +266,39 @@ Get the latest version using GO.
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 admin@sshportal config backup > sshportal.bkp
ssh portal config backup > sshportal.bkp
# Restore
ssh admin@sshportal config restore < sshportal.bkp
ssh portal config restore < sshportal.bkp
```
This method is particularly useful as it should be resistant against future DB schema changes (expected during development phase).
@@ -269,3 +312,31 @@ 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
By default, `sshportal` will return `OK` to anyone sshing using the `healthcheck` user without checking for authentication.
```console
$ ssh healthcheck@sshportal
OK
$
```
the `healtcheck` user can be changed using the `healthcheck-user` option.

4
acl.go
View File

@@ -26,10 +26,10 @@ func CheckACLs(user User, host Host) (string, error) {
// deny by default if no shared ACL
if len(aclMap) == 0 {
return "deny", nil // default action
return ACLActionDeny, nil // default action
}
// transofrm map to slice and sort it
// transform map to slice and sort it
acls := []*ACL{}
for _, acl := range aclMap {
acls = append(acls, acl)

View File

@@ -15,17 +15,21 @@ func TestCheckACLs(t *testing.T) {
// create tmp dir
tempDir, err := ioutil.TempDir("", "sshportal")
So(err, ShouldBeNil)
defer os.RemoveAll(tempDir)
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
hostGroup, err := FindHostGroupByIdOrName(db, "default")
var hostGroup HostGroup
err = HostGroupsByIdentifiers(db, []string{"default"}).First(&hostGroup).Error
So(err, ShouldBeNil)
db.Create(&Host{Groups: []HostGroup{*hostGroup}})
db.Create(&Host{Groups: []*HostGroup{&hostGroup}})
//. load db
var (
@@ -38,6 +42,6 @@ func TestCheckACLs(t *testing.T) {
// test
action, err := CheckACLs(users[0], hosts[0])
So(err, ShouldBeNil)
So(action, ShouldEqual, "allow")
So(action, ShouldEqual, ACLActionAllow)
})
}

View File

@@ -2,11 +2,15 @@ package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"strings"
gossh "golang.org/x/crypto/ssh"
@@ -47,3 +51,82 @@ func NewSSHKey(keyType string, length uint) (*SSHKey, error) {
return &key, nil
}
func encrypt(key []byte, text string) (string, error) {
plaintext := []byte(text)
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
return base64.URLEncoding.EncodeToString(ciphertext), nil
}
func decrypt(key []byte, cryptoText string) (string, error) {
ciphertext, _ := base64.URLEncoding.DecodeString(cryptoText)
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
if len(ciphertext) < aes.BlockSize {
return "", fmt.Errorf("ciphertext too short")
}
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(ciphertext, ciphertext)
return fmt.Sprintf("%s", ciphertext), nil
}
func safeDecrypt(key []byte, cryptoText string) string {
if len(key) == 0 {
return cryptoText
}
out, err := decrypt(key, cryptoText)
if err != nil {
return cryptoText
}
return out
}
func HostEncrypt(aesKey string, host *Host) error {
if aesKey == "" {
return nil
}
var err error
if host.Password != "" {
if host.Password, err = encrypt([]byte(aesKey), host.Password); err != nil {
return err
}
}
return nil
}
func HostDecrypt(aesKey string, host *Host) {
if aesKey == "" {
return
}
if host.Password != "" {
host.Password = safeDecrypt([]byte(aesKey), host.Password)
}
}
func SSHKeyEncrypt(aesKey string, key *SSHKey) error {
if aesKey == "" {
return nil
}
var err error
key.PrivKey, err = encrypt([]byte(aesKey), key.PrivKey)
return err
}
func SSHKeyDecrypt(aesKey string, key *SSHKey) {
if aesKey == "" {
return
}
key.PrivKey = safeDecrypt([]byte(aesKey), key.PrivKey)
}

140
db.go
View File

@@ -1,7 +1,10 @@
//go:generate stringer -type=SessionStatus
package main
import (
"encoding/json"
"fmt"
"log"
"net/url"
"regexp"
"strings"
@@ -21,7 +24,10 @@ type Config struct {
HostGroups []*HostGroup `json:"host_groups"`
ACLs []*ACL `json:"acls"`
Settings []*Setting `json:"settings"`
Date time.Time `json:"date"`
Events []*Event `json:"events"`
Sessions []*Session `json:"sessions"`
// FIXME: add latest migration
Date time.Time `json:"date"`
}
type Setting struct {
@@ -30,6 +36,7 @@ type Setting struct {
Value string `valid:"required"`
}
// SSHKey defines a ssh client key (used by sshportal to connect to remote hosts)
type SSHKey struct {
// FIXME: use uuid for ID
gorm.Model
@@ -46,23 +53,25 @@ 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"`
Addr string `valid:"required"`
User string `valid:"optional"`
Password string `valid:"optional"`
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
Fingerprint string `valid:"optional"` // FIXME: replace with hostKey ?
Comment string `valid:"optional"`
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;"`
Comment string `valid:"optional"`
}
// 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)"`
UserID uint ``
User *User `gorm:"ForeignKey:UserID"`
Comment string `valid:"optional"`
Key []byte `sql:"size:10000" valid:"required,length(1|10000)"`
AuthorizedKey string `sql:"size:10000" valid:"required,length(1|10000)"`
UserID uint ``
User *User `gorm:"ForeignKey:UserID"`
Comment string `valid:"optional"`
}
type UserRole struct {
@@ -109,6 +118,45 @@ type ACL struct {
Comment string `valid:"optional"`
}
type Session struct {
gorm.Model
StoppedAt *time.Time `sql:"index" valid:"optional"`
Status string `valid:"required"`
User *User `gorm:"ForeignKey:UserID"`
Host *Host `gorm:"ForeignKey:HostID"`
UserID uint `valid:"optional"`
HostID uint `valid:"optional"`
ErrMsg string `valid:"optional"`
Comment string `valid:"optional"`
}
type Event struct {
gorm.Model
Author *User `gorm:"ForeignKey:AuthorID"`
AuthorID uint `valid:"optional"`
Domain string `valid:"required"`
Action string `valid:"required"`
Entity string `valid:"optional"`
Args []byte `sql:"size:10000" valid:"optional,length(1|10000)" json:"-"`
ArgsMap map[string]interface{} `gorm:"-" json:"Args"`
}
type SessionStatus string
const (
SessionStatusUnknown SessionStatus = "unknown"
SessionStatusActive = "active"
SessionStatusClosed = "closed"
)
type ACLAction string
const (
ACLActionUnknown ACLAction = "unknown"
ACLActionAllow = "allow"
ACLActionDeny = "deny"
)
func init() {
unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*")
@@ -161,6 +209,7 @@ func (host *Host) Hostname() string {
}
// Host helpers
func HostsPreload(db *gorm.DB) *gorm.DB {
return db.Preload("Groups").Preload("SSHKey")
}
@@ -169,6 +218,7 @@ func HostsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
}
// SSHKey helpers
func SSHKeysPreload(db *gorm.DB) *gorm.DB {
return db.Preload("Hosts")
}
@@ -177,6 +227,7 @@ func SSHKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
}
// HostGroup helpers
func HostGroupsPreload(db *gorm.DB) *gorm.DB {
return db.Preload("ACLs").Preload("Hosts")
}
@@ -184,7 +235,8 @@ func HostGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
}
// UserGroup heleprs
// UserGroup helpers
func UserGroupsPreload(db *gorm.DB) *gorm.DB {
return db.Preload("ACLs").Preload("Users")
}
@@ -193,6 +245,7 @@ func UserGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
}
// User helpers
func UsersPreload(db *gorm.DB) *gorm.DB {
return db.Preload("Groups").Preload("Keys").Preload("Roles")
}
@@ -222,6 +275,7 @@ func UserCheckRoles(user User, names []string) error {
}
// ACL helpers
func ACLsPreload(db *gorm.DB) *gorm.DB {
return db.Preload("UserGroups").Preload("HostGroups")
}
@@ -230,6 +284,7 @@ func ACLsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
}
// UserKey helpers
func UserKeysPreload(db *gorm.DB) *gorm.DB {
return db.Preload("User")
}
@@ -238,9 +293,64 @@ func UserKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
}
// UserRole helpers
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)
}
// Session helpers
func SessionsPreload(db *gorm.DB) *gorm.DB {
return db.Preload("User").Preload("Host")
}
func SessionsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
return db.Where("id IN (?)", identifiers)
}
// Events helpers
func EventsPreload(db *gorm.DB) *gorm.DB {
return db.Preload("Author")
}
func EventsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
return db.Where("id IN (?)", identifiers)
}
func NewEvent(domain, action string) *Event {
return &Event{
Domain: domain,
Action: action,
ArgsMap: map[string]interface{}{},
}
}
func (e *Event) String() string {
return fmt.Sprintf("%s %s %s %s", e.Domain, e.Action, e.Entity, string(e.Args))
}
func (e *Event) Log(db *gorm.DB) {
if len(e.ArgsMap) > 0 {
var err error
if e.Args, err = json.Marshal(e.ArgsMap); err != nil {
log.Printf("error: %v", err)
}
}
log.Printf("info: %s", e)
if err := db.Create(e).Error; err != nil {
log.Printf("warning: %v", err)
}
}
func (e *Event) SetAuthor(user *User) *Event {
e.Author = user
e.AuthorID = user.ID
return e
}
func (e *Event) SetArg(name string, value interface{}) *Event {
e.ArgsMap[name] = value
return e
}

214
dbinit.go
View File

@@ -2,14 +2,21 @@ package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"time"
"github.com/go-gormigrate/gormigrate"
"github.com/jinzhu/gorm"
gossh "golang.org/x/crypto/ssh"
)
func dbInit(db *gorm.DB) error {
log.SetOutput(ioutil.Discard)
db.Callback().Delete().Replace("gorm:delete", hardDeleteCallback)
log.SetOutput(os.Stderr)
m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
{
ID: "1",
@@ -24,8 +31,7 @@ func dbInit(db *gorm.DB) error {
Rollback: func(tx *gorm.DB) error {
return tx.DropTable("settings").Error
},
},
{
}, {
ID: "2",
Migrate: func(tx *gorm.DB) error {
type SSHKey struct {
@@ -152,7 +158,7 @@ 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{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_settings_name", "name").Error
return db.Model(&Setting{}).AddUniqueIndex("uix_settings_name", "name").Error
},
Rollback: func(tx *gorm.DB) error {
return db.Model(&Setting{}).RemoveIndex("uix_settings_name").Error
@@ -161,7 +167,7 @@ func dbInit(db *gorm.DB) error {
ID: "10",
Migrate: func(tx *gorm.DB) error {
db.Model(&SSHKey{}).RemoveIndex("uix_keys_name")
return db.Model(&SSHKey{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_keys_name", "name").Error
return db.Model(&SSHKey{}).AddUniqueIndex("uix_keys_name", "name").Error
},
Rollback: func(tx *gorm.DB) error {
return db.Model(&SSHKey{}).RemoveIndex("uix_keys_name").Error
@@ -170,7 +176,7 @@ func dbInit(db *gorm.DB) error {
ID: "11",
Migrate: func(tx *gorm.DB) error {
db.Model(&Host{}).RemoveIndex("uix_hosts_name")
return db.Model(&Host{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_hosts_name", "name").Error
return db.Model(&Host{}).AddUniqueIndex("uix_hosts_name", "name").Error
},
Rollback: func(tx *gorm.DB) error {
return db.Model(&Host{}).RemoveIndex("uix_hosts_name").Error
@@ -179,7 +185,7 @@ func dbInit(db *gorm.DB) error {
ID: "12",
Migrate: func(tx *gorm.DB) error {
db.Model(&User{}).RemoveIndex("uix_users_name")
return db.Model(&User{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_users_name", "name").Error
return db.Model(&User{}).AddUniqueIndex("uix_users_name", "name").Error
},
Rollback: func(tx *gorm.DB) error {
return db.Model(&User{}).RemoveIndex("uix_users_name").Error
@@ -188,7 +194,7 @@ func dbInit(db *gorm.DB) error {
ID: "13",
Migrate: func(tx *gorm.DB) error {
db.Model(&UserGroup{}).RemoveIndex("uix_usergroups_name")
return db.Model(&UserGroup{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_usergroups_name", "name").Error
return db.Model(&UserGroup{}).AddUniqueIndex("uix_usergroups_name", "name").Error
},
Rollback: func(tx *gorm.DB) error {
return db.Model(&UserGroup{}).RemoveIndex("uix_usergroups_name").Error
@@ -197,7 +203,7 @@ func dbInit(db *gorm.DB) error {
ID: "14",
Migrate: func(tx *gorm.DB) error {
db.Model(&HostGroup{}).RemoveIndex("uix_hostgroups_name")
return db.Model(&HostGroup{}).Where(`"deleted_at" IS NULL`).AddUniqueIndex("uix_hostgroups_name", "name").Error
return db.Model(&HostGroup{}).AddUniqueIndex("uix_hostgroups_name", "name").Error
},
Rollback: func(tx *gorm.DB) error {
return db.Model(&HostGroup{}).RemoveIndex("uix_hostgroups_name").Error
@@ -292,11 +298,148 @@ func dbInit(db *gorm.DB) error {
Rollback: func(tx *gorm.DB) error {
return tx.Where("name = ?", "listhosts").Delete(&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"`
}
return tx.AutoMigrate(&Session{}).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.DropTable("sessions").Error
},
}, {
ID: "22",
Migrate: func(tx *gorm.DB) error {
type Event struct {
gorm.Model
Author *User `gorm:"ForeignKey:AuthorID"`
AuthorID uint `valid:"optional"`
Domain string `valid:"required"`
Action string `valid:"required"`
Entity string `valid:"optional"`
Args []byte `sql:"size:10000" valid:"optional,length(1|10000)"`
}
return tx.AutoMigrate(&Event{}).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.DropTable("events").Error
},
}, {
ID: "23",
Migrate: func(tx *gorm.DB) error {
type UserKey struct {
gorm.Model
Key []byte `sql:"size: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"`
}
return tx.AutoMigrate(&UserKey{}).Error
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "24",
Migrate: func(tx *gorm.DB) error {
var userKeys []UserKey
if err := db.Find(&userKeys).Error; err != nil {
return err
}
for _, userKey := range userKeys {
key, err := gossh.ParsePublicKey(userKey.Key)
if err != nil {
return err
}
userKey.AuthorizedKey = string(gossh.MarshalAuthorizedKey(key))
if err := db.Model(&userKey).Updates(&userKey).Error; err != nil {
return err
}
}
return nil
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "25",
Migrate: func(tx *gorm.DB) error {
type Host struct {
// 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"`
}
return tx.AutoMigrate(&Host{}).Error
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "26",
Migrate: func(tx *gorm.DB) error {
type Session struct {
gorm.Model
StoppedAt *time.Time `sql:"index" valid:"optional"`
Status string `valid:"required"`
User *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"`
}
return tx.AutoMigrate(&Session{}).Error
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "27",
Migrate: func(tx *gorm.DB) error {
var sessions []Session
if err := db.Find(&sessions).Error; err != nil {
return err
}
for _, session := range sessions {
if session.StoppedAt != nil && session.StoppedAt.IsZero() {
if err := db.Model(&session).Updates(map[string]interface{}{"stopped_at": nil}).Error; err != nil {
return err
}
}
}
return nil
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
},
})
if err := m.Migrate(); err != nil {
return err
}
NewEvent("system", "migrated").Log(db)
// create default ssh key
var count uint
@@ -371,7 +514,7 @@ func dbInit(db *gorm.DB) error {
db.Table("users").Count(&count)
if count == 0 {
// if no admin, create an account for the first connection
inviteToken := RandStringBytes(16)
inviteToken := randStringBytes(16)
if os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN") != "" {
inviteToken = os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN")
}
@@ -380,7 +523,7 @@ func dbInit(db *gorm.DB) error {
return err
}
user := User{
Name: "Administrator",
Name: "admin",
Email: "admin@sshportal",
Comment: "created by sshportal",
Roles: []*UserRole{&adminRole},
@@ -390,7 +533,7 @@ func dbInit(db *gorm.DB) error {
if err := db.Create(&user).Error; err != nil {
return err
}
log.Printf("Admin user created, use the user 'invite:%s' to associate a public key with this account", user.InviteToken)
log.Printf("info 'admin' user created, use the user 'invite:%s' to associate a public key with this account", user.InviteToken)
}
// create host ssh key
@@ -408,29 +551,38 @@ func dbInit(db *gorm.DB) error {
return err
}
}
return nil
}
func dbDemo(db *gorm.DB) error {
var hostGroup HostGroup
if err := HostGroupsByIdentifiers(db, []string{"default"}).First(&hostGroup).Error; err != nil {
// close unclosed connections
if err := db.Table("sessions").Where("status = ?", "active").Updates(&Session{
Status: SessionStatusClosed,
ErrMsg: "sshportal was halted while the connection was still active",
}).Error; err != nil {
return err
}
var key SSHKey
if err := SSHKeysByIdentifiers(db, []string{"default"}).First(&key).Error; err != nil {
return err
}
var (
host1 = Host{Name: "sdf", Addr: "sdf.org:22", User: "new", SSHKeyID: key.ID, Groups: []*HostGroup{&hostGroup}}
host2 = Host{Name: "whoami", Addr: "whoami.filippo.io:22", User: "test", SSHKeyID: key.ID, Groups: []*HostGroup{&hostGroup}}
host3 = Host{Name: "ssh-chat", Addr: "chat.shazow.net:22", User: "test", SSHKeyID: key.ID, Fingerprint: "MD5:e5:d5:d1:75:90:38:42:f6:c7:03:d7:d0:56:7d:6a:db", Groups: []*HostGroup{&hostGroup}}
)
// FIXME: check if hosts exist to avoid `UNIQUE constraint` error
db.FirstOrCreate(&host1)
db.FirstOrCreate(&host2)
db.FirstOrCreate(&host3)
return nil
}
func hardDeleteCallback(scope *gorm.Scope) {
if !scope.HasError() {
var extraOption string
if str, ok := scope.Get("gorm:delete_option"); ok {
extraOption = fmt.Sprint(str)
}
/* #nosec */
scope.Raw(fmt.Sprintf(
"DELETE FROM %v%v%v",
scope.QuotedTableName(),
addExtraSpaceIfExist(scope.CombinedConditionSql()),
addExtraSpaceIfExist(extraOption),
)).Exec()
}
}
func addExtraSpaceIfExist(str string) string {
if str != "" {
return " " + str
}
return ""
}

View File

@@ -76,7 +76,12 @@ xssh -l admin host ls
xssh -l admin config backup --indent > backup-1
xssh -l admin config restore --confirm < backup-1
xssh -l admin config backup --indent > backup-2
diff <(cat backup-1 | grep -v '"date":') <(cat backup-2 | grep -v '"date":')
(
cat backup-1 | grep -v '"date":' > backup-1.clean
cat backup-2 | grep -v '"date":' > backup-2.clean
set -xe
diff backup-1.clean backup-2.clean
)
# post cleanup
#cleanup

120
main.go
View File

@@ -19,14 +19,14 @@ import (
)
var (
// VERSION should be updated by hand at each release
VERSION = "1.3.0"
// GIT_TAG will be overwritten automatically by the build system
GIT_TAG string
// GIT_SHA will be overwritten automatically by the build system
GIT_SHA string
// GIT_BRANCH will be overwritten automatically by the build system
GIT_BRANCH string
// Version should be updated by hand at each release
Version = "1.6.0"
// GitTag will be overwritten automatically by the build system
GitTag string
// GitSha will be overwritten automatically by the build system
GitSha string
// GitBranch will be overwritten automatically by the build system
GitBranch string
)
type sshportalContextKey string
@@ -43,7 +43,7 @@ func main() {
app := cli.NewApp()
app.Name = path.Base(os.Args[0])
app.Author = "Manfred Touron"
app.Version = VERSION + " (" + GIT_SHA + ")"
app.Version = Version + " (" + GitSha + ")"
app.Email = "https://github.com/moul/sshportal"
app.Flags = []cli.Flag{
cli.StringFlag{
@@ -52,15 +52,11 @@ func main() {
Value: ":2222",
Usage: "SSH server bind address",
},
cli.BoolFlag{
Name: "demo",
Usage: "*unsafe* - demo mode: accept all connections",
},
/*cli.StringFlag{
cli.StringFlag{
Name: "db-driver",
Value: "sqlite3",
Usage: "GORM driver (sqlite3)",
},*/
},
cli.StringFlag{
Name: "db-conn",
Value: "./sshportal.db",
@@ -75,6 +71,15 @@ func main() {
Usage: "SSH user that spawns a configuration shell",
Value: "admin",
},
cli.StringFlag{
Name: "healthcheck-user",
Usage: "SSH user that returns healthcheck status without checking the SSH key",
Value: "healthcheck",
},
cli.StringFlag{
Name: "aes-key",
Usage: "Encrypt sensitive data in database (length: 16, 24 or 32)",
},
}
app.Action = server
if err := app.Run(os.Args); err != nil {
@@ -83,12 +88,21 @@ func main() {
}
func server(c *cli.Context) error {
switch len(c.String("aes-key")) {
case 0, 16, 24, 32:
default:
return fmt.Errorf("invalid aes key size, should be 16 or 24, 32")
}
// db
db, err := gorm.Open("sqlite3", c.String("db-conn"))
db, err := gorm.Open(c.String("db-driver"), c.String("db-conn"))
if err != nil {
return err
}
defer db.Close()
defer func() {
if err2 := db.Close(); err2 != nil {
panic(err2)
}
}()
if err = db.DB().Ping(); err != nil {
return err
}
@@ -98,11 +112,6 @@ func server(c *cli.Context) error {
if err := dbInit(db); err != nil {
return err
}
if c.Bool("demo") {
if err := dbDemo(db); err != nil {
return err
}
}
// ssh server
ssh.Handle(func(s ssh.Session) {
@@ -119,6 +128,9 @@ func server(c *cli.Context) error {
}
switch username := s.User(); {
case username == c.String("healthcheck-user"):
fmt.Fprintln(s, "OK")
return
case username == currentUser.Name || username == currentUser.Email || username == c.String("config-user"):
if err := shell(c, s, s.Command(), db); err != nil {
fmt.Fprintf(s, "error: %v\n", err)
@@ -136,31 +148,54 @@ func server(c *cli.Context) error {
// load up-to-date objects
// FIXME: cache them or try not to load them
var tmpUser User
if err := db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", currentUser.ID).First(&tmpUser).Error; err != nil {
fmt.Fprintf(s, "error: %v\n", err)
if err2 := db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", currentUser.ID).First(&tmpUser).Error; err2 != nil {
fmt.Fprintf(s, "error: %v\n", err2)
return
}
var tmpHost Host
if err := db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", host.ID).First(&tmpHost).Error; err != nil {
fmt.Fprintf(s, "error: %v\n", err)
if err2 := db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", host.ID).First(&tmpHost).Error; err2 != nil {
fmt.Fprintf(s, "error: %v\n", err2)
return
}
action, err := CheckACLs(tmpUser, tmpHost)
if err != nil {
fmt.Fprintf(s, "error: %v\n", err)
action, err2 := CheckACLs(tmpUser, tmpHost)
if err2 != nil {
fmt.Fprintf(s, "error: %v\n", err2)
return
}
// decrypt key and password
HostDecrypt(c.String("aes-key"), host)
SSHKeyDecrypt(c.String("aes-key"), host.SSHKey)
switch action {
case "allow":
if err := proxy(s, host); err != nil {
fmt.Fprintf(s, "error: %v\n", err)
case ACLActionAllow:
sess := Session{
UserID: currentUser.ID,
HostID: host.ID,
Status: SessionStatusActive,
}
case "deny":
if err2 := db.Create(&sess).Error; err2 != nil {
fmt.Fprintf(s, "error: %v\n", err2)
return
}
sessUpdate := Session{}
if err2 := proxy(s, host, DynamicHostKey(db, host)); err2 != nil {
fmt.Fprintf(s, "error: %v\n", err2)
sessUpdate.ErrMsg = fmt.Sprintf("%v", err2)
switch sessUpdate.ErrMsg {
case "lch closed the connection", "rch closed the connection":
sessUpdate.ErrMsg = ""
}
}
sessUpdate.Status = SessionStatusClosed
now := time.Now()
sessUpdate.StoppedAt = &now
db.Model(&sess).Updates(&sessUpdate)
case ACLActionDeny:
fmt.Fprintf(s, "You don't have permission to that host.\n")
default:
fmt.Fprintf(s, "error: %v\n", err)
fmt.Fprintf(s, "error: invalid ACL action: %q\n", action)
}
}
@@ -175,11 +210,11 @@ func server(c *cli.Context) error {
)
// lookup user by key
db.Where("key = ?", key.Marshal()).First(&userKey)
db.Where("authorized_key = ?", string(gossh.MarshalAuthorizedKey(key))).First(&userKey)
if userKey.UserID > 0 {
db.Preload("Roles").Where("id = ?", userKey.UserID).First(&user)
if strings.HasPrefix(username, "invite:") {
ctx.SetValue(errorContextKey, fmt.Errorf("invites are only supported for ney SSH keys; your ssh key is already associated with the user %q.", user.Email))
ctx.SetValue(errorContextKey, fmt.Errorf("invites are only supported for ney SSH keys; your ssh key is already associated with the user %q", user.Email))
}
ctx.SetValue(userContextKey, user)
return true
@@ -193,15 +228,16 @@ func server(c *cli.Context) error {
}
if user.ID > 0 {
userKey = UserKey{
UserID: user.ID,
Key: key.Marshal(),
Comment: "created by sshportal",
UserID: user.ID,
Key: key.Marshal(),
Comment: "created by sshportal",
AuthorizedKey: string(gossh.MarshalAuthorizedKey(key)),
}
db.Create(&userKey)
// token is only usable once
user.InviteToken = ""
db.Update(&user)
db.Model(&user).Updates(&user)
ctx.SetValue(messageContextKey, fmt.Sprintf("Welcome %s!\n\nYour key is now associated with the user %q.\n", user.Name, user.Email))
ctx.SetValue(userContextKey, user)
@@ -223,6 +259,8 @@ func server(c *cli.Context) error {
if err := SSHKeysByIdentifiers(db, []string{"host"}).First(&key).Error; err != nil {
return err
}
SSHKeyDecrypt(c.String("aes-key"), &key)
signer, err := gossh.ParsePrivateKey([]byte(key.PrivKey))
if err != nil {
return err
@@ -231,6 +269,6 @@ func server(c *cli.Context) error {
return nil
})
log.Printf("SSH Server accepting connections on %s", c.String("bind-address"))
log.Printf("info: SSH Server accepting connections on %s", c.String("bind-address"))
return ssh.ListenAndServe(c.String("bind-address"), nil, opts...)
}

View File

@@ -10,8 +10,8 @@ import (
gossh "golang.org/x/crypto/ssh"
)
func proxy(s ssh.Session, host *Host) error {
config, err := host.ClientConfig(s)
func proxy(s ssh.Session, host *Host, hk gossh.HostKeyCallback) error {
config, err := host.clientConfig(s, hk)
if err != nil {
return err
}
@@ -20,21 +20,21 @@ func proxy(s ssh.Session, host *Host) error {
if err != nil {
return err
}
defer rconn.Close()
defer func() { _ = rconn.Close() }()
rch, rreqs, err := rconn.OpenChannel("session", []byte{})
if err != nil {
return err
}
log.Println("SSH Connectin established")
log.Println("SSH Connection established")
return pipe(s.MaskedReqs(), rreqs, s, rch)
}
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel) error {
defer func() {
lch.Close()
rch.Close()
_ = lch.Close()
_ = rch.Close()
}()
errch := make(chan error, 1)
@@ -59,7 +59,9 @@ func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel) error {
if err != nil {
return err
}
req.Reply(b, nil)
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
@@ -68,18 +70,19 @@ func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel) error {
if err != nil {
return err
}
req.Reply(b, nil)
if err2 := req.Reply(b, nil); err2 != nil {
return err2
}
case err := <-errch:
return err
}
}
return nil
}
func (host *Host) ClientConfig(_ ssh.Session) (*gossh.ClientConfig, error) {
func (host *Host) clientConfig(_ ssh.Session, hk gossh.HostKeyCallback) (*gossh.ClientConfig, error) {
config := gossh.ClientConfig{
User: host.User,
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
HostKeyCallback: hk,
Auth: []gossh.AuthMethod{},
}
if host.SSHKey != nil {

706
shell.go

File diff suppressed because it is too large Load Diff

36
ssh.go Normal file
View File

@@ -0,0 +1,36 @@
package main
import (
"bytes"
"fmt"
"log"
"net"
"github.com/jinzhu/gorm"
gossh "golang.org/x/crypto/ssh"
)
type dynamicHostKey struct {
db *gorm.DB
host *Host
}
func (d *dynamicHostKey) check(hostname string, remote net.Addr, key gossh.PublicKey) error {
if len(d.host.HostKey) == 0 {
log.Println("Discovering host fingerprint...")
return d.db.Model(d.host).Update("HostKey", key.Marshal()).Error
}
if !bytes.Equal(d.host.HostKey, key.Marshal()) {
return fmt.Errorf("ssh: host key mismatch")
}
return nil
}
// DynamicHostKey returns a function for use in
// ClientConfig.HostKeyCallback to dynamically learn or accept host key.
func DynamicHostKey(db *gorm.DB, host *Host) gossh.HostKeyCallback {
// FIXME: forward interactively the host key checking
hk := &dynamicHostKey{db, host}
return hk.check
}

View File

@@ -4,10 +4,17 @@ import "math/rand"
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func RandStringBytes(n int) string {
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] + "..."
}

21
vendor/github.com/dustin/go-humanize/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
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 Normal file
View File

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

31
vendor/github.com/dustin/go-humanize/big.go generated vendored Normal file
View File

@@ -0,0 +1,31 @@
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 Normal file
View File

@@ -0,0 +1,173 @@
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 Normal file
View File

@@ -0,0 +1,143 @@
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 Normal file
View File

@@ -0,0 +1,108 @@
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 Normal file
View File

@@ -0,0 +1,40 @@
// +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 Normal file
View File

@@ -0,0 +1,23 @@
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 Normal file
View File

@@ -0,0 +1,8 @@
/*
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 Normal file
View File

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

25
vendor/github.com/dustin/go-humanize/ordinals.go generated vendored Normal file
View File

@@ -0,0 +1,25 @@
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 Normal file
View File

@@ -0,0 +1,113 @@
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 Normal file
View File

@@ -0,0 +1,117 @@
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...)
}

21
vendor/github.com/mattn/go-colorable/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Yasuhiro Matsumoto
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.

48
vendor/github.com/mattn/go-colorable/README.md generated vendored Normal file
View File

@@ -0,0 +1,48 @@
# go-colorable
[![Godoc Reference](https://godoc.org/github.com/mattn/go-colorable?status.svg)](http://godoc.org/github.com/mattn/go-colorable)
[![Build Status](https://travis-ci.org/mattn/go-colorable.svg?branch=master)](https://travis-ci.org/mattn/go-colorable)
[![Coverage Status](https://coveralls.io/repos/github/mattn/go-colorable/badge.svg?branch=master)](https://coveralls.io/github/mattn/go-colorable?branch=master)
[![Go Report Card](https://goreportcard.com/badge/mattn/go-colorable)](https://goreportcard.com/report/mattn/go-colorable)
Colorable writer for windows.
For example, most of logger packages doesn't show colors on windows. (I know we can do it with ansicon. But I don't want.)
This package is possible to handle escape sequence for ansi color on windows.
## Too Bad!
![](https://raw.githubusercontent.com/mattn/go-colorable/gh-pages/bad.png)
## So Good!
![](https://raw.githubusercontent.com/mattn/go-colorable/gh-pages/good.png)
## Usage
```go
logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})
logrus.SetOutput(colorable.NewColorableStdout())
logrus.Info("succeeded")
logrus.Warn("not correct")
logrus.Error("something error")
logrus.Fatal("panic")
```
You can compile above code on non-windows OSs.
## Installation
```
$ go get github.com/mattn/go-colorable
```
# License
MIT
# Author
Yasuhiro Matsumoto (a.k.a mattn)

View File

@@ -0,0 +1,29 @@
// +build appengine
package colorable
import (
"io"
"os"
_ "github.com/mattn/go-isatty"
)
// NewColorable return new instance of Writer which handle escape sequence.
func NewColorable(file *os.File) io.Writer {
if file == nil {
panic("nil passed instead of *os.File to NewColorable()")
}
return file
}
// NewColorableStdout return new instance of Writer which handle escape sequence for stdout.
func NewColorableStdout() io.Writer {
return os.Stdout
}
// NewColorableStderr return new instance of Writer which handle escape sequence for stderr.
func NewColorableStderr() io.Writer {
return os.Stderr
}

View File

@@ -0,0 +1,30 @@
// +build !windows
// +build !appengine
package colorable
import (
"io"
"os"
_ "github.com/mattn/go-isatty"
)
// NewColorable return new instance of Writer which handle escape sequence.
func NewColorable(file *os.File) io.Writer {
if file == nil {
panic("nil passed instead of *os.File to NewColorable()")
}
return file
}
// NewColorableStdout return new instance of Writer which handle escape sequence for stdout.
func NewColorableStdout() io.Writer {
return os.Stdout
}
// NewColorableStderr return new instance of Writer which handle escape sequence for stderr.
func NewColorableStderr() io.Writer {
return os.Stderr
}

View File

@@ -0,0 +1,978 @@
// +build windows
// +build !appengine
package colorable
import (
"bytes"
"io"
"math"
"os"
"strconv"
"strings"
"syscall"
"unsafe"
"github.com/mattn/go-isatty"
)
const (
foregroundBlue = 0x1
foregroundGreen = 0x2
foregroundRed = 0x4
foregroundIntensity = 0x8
foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity)
backgroundBlue = 0x10
backgroundGreen = 0x20
backgroundRed = 0x40
backgroundIntensity = 0x80
backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity)
)
const (
genericRead = 0x80000000
genericWrite = 0x40000000
)
const (
consoleTextmodeBuffer = 0x1
)
type wchar uint16
type short int16
type dword uint32
type word uint16
type coord struct {
x short
y short
}
type smallRect struct {
left short
top short
right short
bottom short
}
type consoleScreenBufferInfo struct {
size coord
cursorPosition coord
attributes word
window smallRect
maximumWindowSize coord
}
type consoleCursorInfo struct {
size dword
visible int32
}
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute")
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute")
procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo")
procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo")
procSetConsoleTitle = kernel32.NewProc("SetConsoleTitleW")
procCreateConsoleScreenBuffer = kernel32.NewProc("CreateConsoleScreenBuffer")
)
// Writer provide colorable Writer to the console
type Writer struct {
out io.Writer
handle syscall.Handle
althandle syscall.Handle
oldattr word
oldpos coord
rest bytes.Buffer
}
// NewColorable return new instance of Writer which handle escape sequence from File.
func NewColorable(file *os.File) io.Writer {
if file == nil {
panic("nil passed instead of *os.File to NewColorable()")
}
if isatty.IsTerminal(file.Fd()) {
var csbi consoleScreenBufferInfo
handle := syscall.Handle(file.Fd())
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
return &Writer{out: file, handle: handle, oldattr: csbi.attributes, oldpos: coord{0, 0}}
}
return file
}
// NewColorableStdout return new instance of Writer which handle escape sequence for stdout.
func NewColorableStdout() io.Writer {
return NewColorable(os.Stdout)
}
// NewColorableStderr return new instance of Writer which handle escape sequence for stderr.
func NewColorableStderr() io.Writer {
return NewColorable(os.Stderr)
}
var color256 = map[int]int{
0: 0x000000,
1: 0x800000,
2: 0x008000,
3: 0x808000,
4: 0x000080,
5: 0x800080,
6: 0x008080,
7: 0xc0c0c0,
8: 0x808080,
9: 0xff0000,
10: 0x00ff00,
11: 0xffff00,
12: 0x0000ff,
13: 0xff00ff,
14: 0x00ffff,
15: 0xffffff,
16: 0x000000,
17: 0x00005f,
18: 0x000087,
19: 0x0000af,
20: 0x0000d7,
21: 0x0000ff,
22: 0x005f00,
23: 0x005f5f,
24: 0x005f87,
25: 0x005faf,
26: 0x005fd7,
27: 0x005fff,
28: 0x008700,
29: 0x00875f,
30: 0x008787,
31: 0x0087af,
32: 0x0087d7,
33: 0x0087ff,
34: 0x00af00,
35: 0x00af5f,
36: 0x00af87,
37: 0x00afaf,
38: 0x00afd7,
39: 0x00afff,
40: 0x00d700,
41: 0x00d75f,
42: 0x00d787,
43: 0x00d7af,
44: 0x00d7d7,
45: 0x00d7ff,
46: 0x00ff00,
47: 0x00ff5f,
48: 0x00ff87,
49: 0x00ffaf,
50: 0x00ffd7,
51: 0x00ffff,
52: 0x5f0000,
53: 0x5f005f,
54: 0x5f0087,
55: 0x5f00af,
56: 0x5f00d7,
57: 0x5f00ff,
58: 0x5f5f00,
59: 0x5f5f5f,
60: 0x5f5f87,
61: 0x5f5faf,
62: 0x5f5fd7,
63: 0x5f5fff,
64: 0x5f8700,
65: 0x5f875f,
66: 0x5f8787,
67: 0x5f87af,
68: 0x5f87d7,
69: 0x5f87ff,
70: 0x5faf00,
71: 0x5faf5f,
72: 0x5faf87,
73: 0x5fafaf,
74: 0x5fafd7,
75: 0x5fafff,
76: 0x5fd700,
77: 0x5fd75f,
78: 0x5fd787,
79: 0x5fd7af,
80: 0x5fd7d7,
81: 0x5fd7ff,
82: 0x5fff00,
83: 0x5fff5f,
84: 0x5fff87,
85: 0x5fffaf,
86: 0x5fffd7,
87: 0x5fffff,
88: 0x870000,
89: 0x87005f,
90: 0x870087,
91: 0x8700af,
92: 0x8700d7,
93: 0x8700ff,
94: 0x875f00,
95: 0x875f5f,
96: 0x875f87,
97: 0x875faf,
98: 0x875fd7,
99: 0x875fff,
100: 0x878700,
101: 0x87875f,
102: 0x878787,
103: 0x8787af,
104: 0x8787d7,
105: 0x8787ff,
106: 0x87af00,
107: 0x87af5f,
108: 0x87af87,
109: 0x87afaf,
110: 0x87afd7,
111: 0x87afff,
112: 0x87d700,
113: 0x87d75f,
114: 0x87d787,
115: 0x87d7af,
116: 0x87d7d7,
117: 0x87d7ff,
118: 0x87ff00,
119: 0x87ff5f,
120: 0x87ff87,
121: 0x87ffaf,
122: 0x87ffd7,
123: 0x87ffff,
124: 0xaf0000,
125: 0xaf005f,
126: 0xaf0087,
127: 0xaf00af,
128: 0xaf00d7,
129: 0xaf00ff,
130: 0xaf5f00,
131: 0xaf5f5f,
132: 0xaf5f87,
133: 0xaf5faf,
134: 0xaf5fd7,
135: 0xaf5fff,
136: 0xaf8700,
137: 0xaf875f,
138: 0xaf8787,
139: 0xaf87af,
140: 0xaf87d7,
141: 0xaf87ff,
142: 0xafaf00,
143: 0xafaf5f,
144: 0xafaf87,
145: 0xafafaf,
146: 0xafafd7,
147: 0xafafff,
148: 0xafd700,
149: 0xafd75f,
150: 0xafd787,
151: 0xafd7af,
152: 0xafd7d7,
153: 0xafd7ff,
154: 0xafff00,
155: 0xafff5f,
156: 0xafff87,
157: 0xafffaf,
158: 0xafffd7,
159: 0xafffff,
160: 0xd70000,
161: 0xd7005f,
162: 0xd70087,
163: 0xd700af,
164: 0xd700d7,
165: 0xd700ff,
166: 0xd75f00,
167: 0xd75f5f,
168: 0xd75f87,
169: 0xd75faf,
170: 0xd75fd7,
171: 0xd75fff,
172: 0xd78700,
173: 0xd7875f,
174: 0xd78787,
175: 0xd787af,
176: 0xd787d7,
177: 0xd787ff,
178: 0xd7af00,
179: 0xd7af5f,
180: 0xd7af87,
181: 0xd7afaf,
182: 0xd7afd7,
183: 0xd7afff,
184: 0xd7d700,
185: 0xd7d75f,
186: 0xd7d787,
187: 0xd7d7af,
188: 0xd7d7d7,
189: 0xd7d7ff,
190: 0xd7ff00,
191: 0xd7ff5f,
192: 0xd7ff87,
193: 0xd7ffaf,
194: 0xd7ffd7,
195: 0xd7ffff,
196: 0xff0000,
197: 0xff005f,
198: 0xff0087,
199: 0xff00af,
200: 0xff00d7,
201: 0xff00ff,
202: 0xff5f00,
203: 0xff5f5f,
204: 0xff5f87,
205: 0xff5faf,
206: 0xff5fd7,
207: 0xff5fff,
208: 0xff8700,
209: 0xff875f,
210: 0xff8787,
211: 0xff87af,
212: 0xff87d7,
213: 0xff87ff,
214: 0xffaf00,
215: 0xffaf5f,
216: 0xffaf87,
217: 0xffafaf,
218: 0xffafd7,
219: 0xffafff,
220: 0xffd700,
221: 0xffd75f,
222: 0xffd787,
223: 0xffd7af,
224: 0xffd7d7,
225: 0xffd7ff,
226: 0xffff00,
227: 0xffff5f,
228: 0xffff87,
229: 0xffffaf,
230: 0xffffd7,
231: 0xffffff,
232: 0x080808,
233: 0x121212,
234: 0x1c1c1c,
235: 0x262626,
236: 0x303030,
237: 0x3a3a3a,
238: 0x444444,
239: 0x4e4e4e,
240: 0x585858,
241: 0x626262,
242: 0x6c6c6c,
243: 0x767676,
244: 0x808080,
245: 0x8a8a8a,
246: 0x949494,
247: 0x9e9e9e,
248: 0xa8a8a8,
249: 0xb2b2b2,
250: 0xbcbcbc,
251: 0xc6c6c6,
252: 0xd0d0d0,
253: 0xdadada,
254: 0xe4e4e4,
255: 0xeeeeee,
}
// `\033]0;TITLESTR\007`
func doTitleSequence(er *bytes.Reader) error {
var c byte
var err error
c, err = er.ReadByte()
if err != nil {
return err
}
if c != '0' && c != '2' {
return nil
}
c, err = er.ReadByte()
if err != nil {
return err
}
if c != ';' {
return nil
}
title := make([]byte, 0, 80)
for {
c, err = er.ReadByte()
if err != nil {
return err
}
if c == 0x07 || c == '\n' {
break
}
title = append(title, c)
}
if len(title) > 0 {
title8, err := syscall.UTF16PtrFromString(string(title))
if err == nil {
procSetConsoleTitle.Call(uintptr(unsafe.Pointer(title8)))
}
}
return nil
}
// Write write data on console
func (w *Writer) Write(data []byte) (n int, err error) {
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
handle := w.handle
var er *bytes.Reader
if w.rest.Len() > 0 {
var rest bytes.Buffer
w.rest.WriteTo(&rest)
w.rest.Reset()
rest.Write(data)
er = bytes.NewReader(rest.Bytes())
} else {
er = bytes.NewReader(data)
}
var bw [1]byte
loop:
for {
c1, err := er.ReadByte()
if err != nil {
break loop
}
if c1 != 0x1b {
bw[0] = c1
w.out.Write(bw[:])
continue
}
c2, err := er.ReadByte()
if err != nil {
break loop
}
switch c2 {
case ']':
w.rest.WriteByte(c1)
w.rest.WriteByte(c2)
er.WriteTo(&w.rest)
if bytes.IndexByte(w.rest.Bytes(), 0x07) == -1 {
break loop
}
er = bytes.NewReader(w.rest.Bytes()[2:])
err := doTitleSequence(er)
if err != nil {
break loop
}
w.rest.Reset()
continue
// https://github.com/mattn/go-colorable/issues/27
case '7':
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
w.oldpos = csbi.cursorPosition
continue
case '8':
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&w.oldpos)))
continue
case 0x5b:
// execute part after switch
default:
continue
}
w.rest.WriteByte(c1)
w.rest.WriteByte(c2)
er.WriteTo(&w.rest)
var buf bytes.Buffer
var m byte
for i, c := range w.rest.Bytes()[2:] {
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
m = c
er = bytes.NewReader(w.rest.Bytes()[2+i+1:])
w.rest.Reset()
break
}
buf.Write([]byte(string(c)))
}
if m == 0 {
break loop
}
switch m {
case 'A':
n, err = strconv.Atoi(buf.String())
if err != nil {
continue
}
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.y -= short(n)
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'B':
n, err = strconv.Atoi(buf.String())
if err != nil {
continue
}
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.y += short(n)
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'C':
n, err = strconv.Atoi(buf.String())
if err != nil {
continue
}
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.x += short(n)
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'D':
n, err = strconv.Atoi(buf.String())
if err != nil {
continue
}
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.x -= short(n)
if csbi.cursorPosition.x < 0 {
csbi.cursorPosition.x = 0
}
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'E':
n, err = strconv.Atoi(buf.String())
if err != nil {
continue
}
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.x = 0
csbi.cursorPosition.y += short(n)
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'F':
n, err = strconv.Atoi(buf.String())
if err != nil {
continue
}
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.x = 0
csbi.cursorPosition.y -= short(n)
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'G':
n, err = strconv.Atoi(buf.String())
if err != nil {
continue
}
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.x = short(n - 1)
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'H', 'f':
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
if buf.Len() > 0 {
token := strings.Split(buf.String(), ";")
switch len(token) {
case 1:
n1, err := strconv.Atoi(token[0])
if err != nil {
continue
}
csbi.cursorPosition.y = short(n1 - 1)
case 2:
n1, err := strconv.Atoi(token[0])
if err != nil {
continue
}
n2, err := strconv.Atoi(token[1])
if err != nil {
continue
}
csbi.cursorPosition.x = short(n2 - 1)
csbi.cursorPosition.y = short(n1 - 1)
}
} else {
csbi.cursorPosition.y = 0
}
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'J':
n := 0
if buf.Len() > 0 {
n, err = strconv.Atoi(buf.String())
if err != nil {
continue
}
}
var count, written dword
var cursor coord
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
switch n {
case 0:
cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y}
count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.size.y-csbi.cursorPosition.y)*csbi.size.x)
case 1:
cursor = coord{x: csbi.window.left, y: csbi.window.top}
count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.window.top-csbi.cursorPosition.y)*csbi.size.x)
case 2:
cursor = coord{x: csbi.window.left, y: csbi.window.top}
count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.size.y-csbi.cursorPosition.y)*csbi.size.x)
}
procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
case 'K':
n := 0
if buf.Len() > 0 {
n, err = strconv.Atoi(buf.String())
if err != nil {
continue
}
}
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
var cursor coord
var count, written dword
switch n {
case 0:
cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y}
count = dword(csbi.size.x - csbi.cursorPosition.x)
case 1:
cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y}
count = dword(csbi.size.x - csbi.cursorPosition.x)
case 2:
cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y}
count = dword(csbi.size.x)
}
procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
case 'm':
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
attr := csbi.attributes
cs := buf.String()
if cs == "" {
procSetConsoleTextAttribute.Call(uintptr(handle), uintptr(w.oldattr))
continue
}
token := strings.Split(cs, ";")
for i := 0; i < len(token); i++ {
ns := token[i]
if n, err = strconv.Atoi(ns); err == nil {
switch {
case n == 0 || n == 100:
attr = w.oldattr
case 1 <= n && n <= 5:
attr |= foregroundIntensity
case n == 7:
attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4)
case n == 22 || n == 25:
attr |= foregroundIntensity
case n == 27:
attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4)
case 30 <= n && n <= 37:
attr &= backgroundMask
if (n-30)&1 != 0 {
attr |= foregroundRed
}
if (n-30)&2 != 0 {
attr |= foregroundGreen
}
if (n-30)&4 != 0 {
attr |= foregroundBlue
}
case n == 38: // set foreground color.
if i < len(token)-2 && (token[i+1] == "5" || token[i+1] == "05") {
if n256, err := strconv.Atoi(token[i+2]); err == nil {
if n256foreAttr == nil {
n256setup()
}
attr &= backgroundMask
attr |= n256foreAttr[n256]
i += 2
}
} else if len(token) == 5 && token[i+1] == "2" {
var r, g, b int
r, _ = strconv.Atoi(token[i+2])
g, _ = strconv.Atoi(token[i+3])
b, _ = strconv.Atoi(token[i+4])
i += 4
if r > 127 {
attr |= foregroundRed
}
if g > 127 {
attr |= foregroundGreen
}
if b > 127 {
attr |= foregroundBlue
}
} else {
attr = attr & (w.oldattr & backgroundMask)
}
case n == 39: // reset foreground color.
attr &= backgroundMask
attr |= w.oldattr & foregroundMask
case 40 <= n && n <= 47:
attr &= foregroundMask
if (n-40)&1 != 0 {
attr |= backgroundRed
}
if (n-40)&2 != 0 {
attr |= backgroundGreen
}
if (n-40)&4 != 0 {
attr |= backgroundBlue
}
case n == 48: // set background color.
if i < len(token)-2 && token[i+1] == "5" {
if n256, err := strconv.Atoi(token[i+2]); err == nil {
if n256backAttr == nil {
n256setup()
}
attr &= foregroundMask
attr |= n256backAttr[n256]
i += 2
}
} else if len(token) == 5 && token[i+1] == "2" {
var r, g, b int
r, _ = strconv.Atoi(token[i+2])
g, _ = strconv.Atoi(token[i+3])
b, _ = strconv.Atoi(token[i+4])
i += 4
if r > 127 {
attr |= backgroundRed
}
if g > 127 {
attr |= backgroundGreen
}
if b > 127 {
attr |= backgroundBlue
}
} else {
attr = attr & (w.oldattr & foregroundMask)
}
case n == 49: // reset foreground color.
attr &= foregroundMask
attr |= w.oldattr & backgroundMask
case 90 <= n && n <= 97:
attr = (attr & backgroundMask)
attr |= foregroundIntensity
if (n-90)&1 != 0 {
attr |= foregroundRed
}
if (n-90)&2 != 0 {
attr |= foregroundGreen
}
if (n-90)&4 != 0 {
attr |= foregroundBlue
}
case 100 <= n && n <= 107:
attr = (attr & foregroundMask)
attr |= backgroundIntensity
if (n-100)&1 != 0 {
attr |= backgroundRed
}
if (n-100)&2 != 0 {
attr |= backgroundGreen
}
if (n-100)&4 != 0 {
attr |= backgroundBlue
}
}
procSetConsoleTextAttribute.Call(uintptr(handle), uintptr(attr))
}
}
case 'h':
var ci consoleCursorInfo
cs := buf.String()
if cs == "5>" {
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
ci.visible = 0
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
} else if cs == "?25" {
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
ci.visible = 1
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
} else if cs == "?1049" {
if w.althandle == 0 {
h, _, _ := procCreateConsoleScreenBuffer.Call(uintptr(genericRead|genericWrite), 0, 0, uintptr(consoleTextmodeBuffer), 0, 0)
w.althandle = syscall.Handle(h)
if w.althandle != 0 {
handle = w.althandle
}
}
}
case 'l':
var ci consoleCursorInfo
cs := buf.String()
if cs == "5>" {
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
ci.visible = 1
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
} else if cs == "?25" {
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
ci.visible = 0
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci)))
} else if cs == "?1049" {
if w.althandle != 0 {
syscall.CloseHandle(w.althandle)
w.althandle = 0
handle = w.handle
}
}
case 's':
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
w.oldpos = csbi.cursorPosition
case 'u':
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&w.oldpos)))
}
}
return len(data), nil
}
type consoleColor struct {
rgb int
red bool
green bool
blue bool
intensity bool
}
func (c consoleColor) foregroundAttr() (attr word) {
if c.red {
attr |= foregroundRed
}
if c.green {
attr |= foregroundGreen
}
if c.blue {
attr |= foregroundBlue
}
if c.intensity {
attr |= foregroundIntensity
}
return
}
func (c consoleColor) backgroundAttr() (attr word) {
if c.red {
attr |= backgroundRed
}
if c.green {
attr |= backgroundGreen
}
if c.blue {
attr |= backgroundBlue
}
if c.intensity {
attr |= backgroundIntensity
}
return
}
var color16 = []consoleColor{
{0x000000, false, false, false, false},
{0x000080, false, false, true, false},
{0x008000, false, true, false, false},
{0x008080, false, true, true, false},
{0x800000, true, false, false, false},
{0x800080, true, false, true, false},
{0x808000, true, true, false, false},
{0xc0c0c0, true, true, true, false},
{0x808080, false, false, false, true},
{0x0000ff, false, false, true, true},
{0x00ff00, false, true, false, true},
{0x00ffff, false, true, true, true},
{0xff0000, true, false, false, true},
{0xff00ff, true, false, true, true},
{0xffff00, true, true, false, true},
{0xffffff, true, true, true, true},
}
type hsv struct {
h, s, v float32
}
func (a hsv) dist(b hsv) float32 {
dh := a.h - b.h
switch {
case dh > 0.5:
dh = 1 - dh
case dh < -0.5:
dh = -1 - dh
}
ds := a.s - b.s
dv := a.v - b.v
return float32(math.Sqrt(float64(dh*dh + ds*ds + dv*dv)))
}
func toHSV(rgb int) hsv {
r, g, b := float32((rgb&0xFF0000)>>16)/256.0,
float32((rgb&0x00FF00)>>8)/256.0,
float32(rgb&0x0000FF)/256.0
min, max := minmax3f(r, g, b)
h := max - min
if h > 0 {
if max == r {
h = (g - b) / h
if h < 0 {
h += 6
}
} else if max == g {
h = 2 + (b-r)/h
} else {
h = 4 + (r-g)/h
}
}
h /= 6.0
s := max - min
if max != 0 {
s /= max
}
v := max
return hsv{h: h, s: s, v: v}
}
type hsvTable []hsv
func toHSVTable(rgbTable []consoleColor) hsvTable {
t := make(hsvTable, len(rgbTable))
for i, c := range rgbTable {
t[i] = toHSV(c.rgb)
}
return t
}
func (t hsvTable) find(rgb int) consoleColor {
hsv := toHSV(rgb)
n := 7
l := float32(5.0)
for i, p := range t {
d := hsv.dist(p)
if d < l {
l, n = d, i
}
}
return color16[n]
}
func minmax3f(a, b, c float32) (min, max float32) {
if a < b {
if b < c {
return a, c
} else if a < c {
return a, b
} else {
return c, b
}
} else {
if a < c {
return b, c
} else if b < c {
return b, a
} else {
return c, a
}
}
}
var n256foreAttr []word
var n256backAttr []word
func n256setup() {
n256foreAttr = make([]word, 256)
n256backAttr = make([]word, 256)
t := toHSVTable(color16)
for i, rgb := range color256 {
c := t.find(rgb)
n256foreAttr[i] = c.foregroundAttr()
n256backAttr[i] = c.backgroundAttr()
}
}

55
vendor/github.com/mattn/go-colorable/noncolorable.go generated vendored Normal file
View File

@@ -0,0 +1,55 @@
package colorable
import (
"bytes"
"io"
)
// NonColorable hold writer but remove escape sequence.
type NonColorable struct {
out io.Writer
}
// NewNonColorable return new instance of Writer which remove escape sequence from Writer.
func NewNonColorable(w io.Writer) io.Writer {
return &NonColorable{out: w}
}
// Write write data on console
func (w *NonColorable) Write(data []byte) (n int, err error) {
er := bytes.NewReader(data)
var bw [1]byte
loop:
for {
c1, err := er.ReadByte()
if err != nil {
break loop
}
if c1 != 0x1b {
bw[0] = c1
w.out.Write(bw[:])
continue
}
c2, err := er.ReadByte()
if err != nil {
break loop
}
if c2 != 0x5b {
continue
}
var buf bytes.Buffer
for {
c, err := er.ReadByte()
if err != nil {
break loop
}
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
break
}
buf.Write([]byte(string(c)))
}
}
return len(data), nil
}

9
vendor/github.com/mattn/go-isatty/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,9 @@
Copyright (c) Yasuhiro MATSUMOTO <mattn.jp@gmail.com>
MIT License (Expat)
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.

37
vendor/github.com/mattn/go-isatty/README.md generated vendored Normal file
View File

@@ -0,0 +1,37 @@
# go-isatty
isatty for golang
## Usage
```go
package main
import (
"fmt"
"github.com/mattn/go-isatty"
"os"
)
func main() {
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Println("Is Terminal")
} else {
fmt.Println("Is Not Terminal")
}
}
```
## Installation
```
$ go get github.com/mattn/go-isatty
```
# License
MIT
# Author
Yasuhiro Matsumoto (a.k.a mattn)

2
vendor/github.com/mattn/go-isatty/doc.go generated vendored Normal file
View File

@@ -0,0 +1,2 @@
// Package isatty implements interface to isatty
package isatty

View File

@@ -0,0 +1,9 @@
// +build appengine
package isatty
// IsTerminal returns true if the file descriptor is terminal which
// is always false on on appengine classic which is a sandboxed PaaS.
func IsTerminal(fd uintptr) bool {
return false
}

18
vendor/github.com/mattn/go-isatty/isatty_bsd.go generated vendored Normal file
View File

@@ -0,0 +1,18 @@
// +build darwin freebsd openbsd netbsd dragonfly
// +build !appengine
package isatty
import (
"syscall"
"unsafe"
)
const ioctlReadTermios = syscall.TIOCGETA
// IsTerminal return true if the file descriptor is terminal.
func IsTerminal(fd uintptr) bool {
var termios syscall.Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
}

18
vendor/github.com/mattn/go-isatty/isatty_linux.go generated vendored Normal file
View File

@@ -0,0 +1,18 @@
// +build linux
// +build !appengine
package isatty
import (
"syscall"
"unsafe"
)
const ioctlReadTermios = syscall.TCGETS
// IsTerminal return true if the file descriptor is terminal.
func IsTerminal(fd uintptr) bool {
var termios syscall.Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
}

16
vendor/github.com/mattn/go-isatty/isatty_solaris.go generated vendored Normal file
View File

@@ -0,0 +1,16 @@
// +build solaris
// +build !appengine
package isatty
import (
"golang.org/x/sys/unix"
)
// IsTerminal returns true if the given file descriptor is a terminal.
// see: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c
func IsTerminal(fd uintptr) bool {
var termio unix.Termio
err := unix.IoctlSetTermio(int(fd), unix.TCGETA, &termio)
return err == nil
}

19
vendor/github.com/mattn/go-isatty/isatty_windows.go generated vendored Normal file
View File

@@ -0,0 +1,19 @@
// +build windows
// +build !appengine
package isatty
import (
"syscall"
"unsafe"
)
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
// IsTerminal return true if the file descriptor is terminal.
func IsTerminal(fd uintptr) bool {
var st uint32
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0)
return r != 0 && e == 0
}

9
vendor/github.com/mgutz/ansi/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright (c) 2013 Mario L. Gutierrez
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.

121
vendor/github.com/mgutz/ansi/README.md generated vendored Normal file
View File

@@ -0,0 +1,121 @@
# ansi
Package ansi is a small, fast library to create ANSI colored strings and codes.
## Install
Get it
```sh
go get -u github.com/mgutz/ansi
```
## Example
```go
import "github.com/mgutz/ansi"
// colorize a string, SLOW
msg := ansi.Color("foo", "red+b:white")
// create a FAST closure function to avoid computation of ANSI code
phosphorize := ansi.ColorFunc("green+h:black")
msg = phosphorize("Bring back the 80s!")
msg2 := phospohorize("Look, I'm a CRT!")
// cache escape codes and build strings manually
lime := ansi.ColorCode("green+h:black")
reset := ansi.ColorCode("reset")
fmt.Println(lime, "Bring back the 80s!", reset)
```
Other examples
```go
Color(s, "red") // red
Color(s, "red+b") // red bold
Color(s, "red+B") // red blinking
Color(s, "red+u") // red underline
Color(s, "red+bh") // red bold bright
Color(s, "red:white") // red on white
Color(s, "red+b:white+h") // red bold on white bright
Color(s, "red+B:white+h") // red blink on white bright
Color(s, "off") // turn off ansi codes
```
To view color combinations, from project directory in terminal.
```sh
go test
```
## Style format
```go
"foregroundColor+attributes:backgroundColor+attributes"
```
Colors
* black
* red
* green
* yellow
* blue
* magenta
* cyan
* white
* 0...255 (256 colors)
Foreground Attributes
* B = Blink
* b = bold
* h = high intensity (bright)
* i = inverse
* s = strikethrough
* u = underline
Background Attributes
* h = high intensity (bright)
## Constants
* ansi.Reset
* ansi.DefaultBG
* ansi.DefaultFG
* ansi.Black
* ansi.Red
* ansi.Green
* ansi.Yellow
* ansi.Blue
* ansi.Magenta
* ansi.Cyan
* ansi.White
* ansi.LightBlack
* ansi.LightRed
* ansi.LightGreen
* ansi.LightYellow
* ansi.LightBlue
* ansi.LightMagenta
* ansi.LightCyan
* ansi.LightWhite
## References
Wikipedia ANSI escape codes [Colors](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
General [tips and formatting](http://misc.flogisoft.com/bash/tip_colors_and_formatting)
What about support on Windows? Use [colorable by mattn](https://github.com/mattn/go-colorable).
Ansi and colorable are used by [logxi](https://github.com/mgutz/logxi) to support logging in
color on Windows.
## MIT License
Copyright (c) 2013 Mario Gutierrez mario@mgutz.com
See the file LICENSE for copying permission.

285
vendor/github.com/mgutz/ansi/ansi.go generated vendored Normal file
View File

@@ -0,0 +1,285 @@
package ansi
import (
"bytes"
"fmt"
"strconv"
"strings"
)
const (
black = iota
red
green
yellow
blue
magenta
cyan
white
defaultt = 9
normalIntensityFG = 30
highIntensityFG = 90
normalIntensityBG = 40
highIntensityBG = 100
start = "\033["
bold = "1;"
blink = "5;"
underline = "4;"
inverse = "7;"
strikethrough = "9;"
// Reset is the ANSI reset escape sequence
Reset = "\033[0m"
// DefaultBG is the default background
DefaultBG = "\033[49m"
// DefaultFG is the default foreground
DefaultFG = "\033[39m"
)
// Black FG
var Black string
// Red FG
var Red string
// Green FG
var Green string
// Yellow FG
var Yellow string
// Blue FG
var Blue string
// Magenta FG
var Magenta string
// Cyan FG
var Cyan string
// White FG
var White string
// LightBlack FG
var LightBlack string
// LightRed FG
var LightRed string
// LightGreen FG
var LightGreen string
// LightYellow FG
var LightYellow string
// LightBlue FG
var LightBlue string
// LightMagenta FG
var LightMagenta string
// LightCyan FG
var LightCyan string
// LightWhite FG
var LightWhite string
var (
plain = false
// Colors maps common color names to their ANSI color code.
Colors = map[string]int{
"black": black,
"red": red,
"green": green,
"yellow": yellow,
"blue": blue,
"magenta": magenta,
"cyan": cyan,
"white": white,
"default": defaultt,
}
)
func init() {
for i := 0; i < 256; i++ {
Colors[strconv.Itoa(i)] = i
}
Black = ColorCode("black")
Red = ColorCode("red")
Green = ColorCode("green")
Yellow = ColorCode("yellow")
Blue = ColorCode("blue")
Magenta = ColorCode("magenta")
Cyan = ColorCode("cyan")
White = ColorCode("white")
LightBlack = ColorCode("black+h")
LightRed = ColorCode("red+h")
LightGreen = ColorCode("green+h")
LightYellow = ColorCode("yellow+h")
LightBlue = ColorCode("blue+h")
LightMagenta = ColorCode("magenta+h")
LightCyan = ColorCode("cyan+h")
LightWhite = ColorCode("white+h")
}
// ColorCode returns the ANSI color color code for style.
func ColorCode(style string) string {
return colorCode(style).String()
}
// Gets the ANSI color code for a style.
func colorCode(style string) *bytes.Buffer {
buf := bytes.NewBufferString("")
if plain || style == "" {
return buf
}
if style == "reset" {
buf.WriteString(Reset)
return buf
} else if style == "off" {
return buf
}
foregroundBackground := strings.Split(style, ":")
foreground := strings.Split(foregroundBackground[0], "+")
fgKey := foreground[0]
fg := Colors[fgKey]
fgStyle := ""
if len(foreground) > 1 {
fgStyle = foreground[1]
}
bg, bgStyle := "", ""
if len(foregroundBackground) > 1 {
background := strings.Split(foregroundBackground[1], "+")
bg = background[0]
if len(background) > 1 {
bgStyle = background[1]
}
}
buf.WriteString(start)
base := normalIntensityFG
if len(fgStyle) > 0 {
if strings.Contains(fgStyle, "b") {
buf.WriteString(bold)
}
if strings.Contains(fgStyle, "B") {
buf.WriteString(blink)
}
if strings.Contains(fgStyle, "u") {
buf.WriteString(underline)
}
if strings.Contains(fgStyle, "i") {
buf.WriteString(inverse)
}
if strings.Contains(fgStyle, "s") {
buf.WriteString(strikethrough)
}
if strings.Contains(fgStyle, "h") {
base = highIntensityFG
}
}
// if 256-color
n, err := strconv.Atoi(fgKey)
if err == nil {
fmt.Fprintf(buf, "38;5;%d;", n)
} else {
fmt.Fprintf(buf, "%d;", base+fg)
}
base = normalIntensityBG
if len(bg) > 0 {
if strings.Contains(bgStyle, "h") {
base = highIntensityBG
}
// if 256-color
n, err := strconv.Atoi(bg)
if err == nil {
fmt.Fprintf(buf, "48;5;%d;", n)
} else {
fmt.Fprintf(buf, "%d;", base+Colors[bg])
}
}
// remove last ";"
buf.Truncate(buf.Len() - 1)
buf.WriteRune('m')
return buf
}
// Color colors a string based on the ANSI color code for style.
func Color(s, style string) string {
if plain || len(style) < 1 {
return s
}
buf := colorCode(style)
buf.WriteString(s)
buf.WriteString(Reset)
return buf.String()
}
// ColorFunc creates a closure to avoid computation ANSI color code.
func ColorFunc(style string) func(string) string {
if style == "" {
return func(s string) string {
return s
}
}
color := ColorCode(style)
return func(s string) string {
if plain || s == "" {
return s
}
buf := bytes.NewBufferString(color)
buf.WriteString(s)
buf.WriteString(Reset)
result := buf.String()
return result
}
}
// DisableColors disables ANSI color codes. The default is false (colors are on).
func DisableColors(disable bool) {
plain = disable
if plain {
Black = ""
Red = ""
Green = ""
Yellow = ""
Blue = ""
Magenta = ""
Cyan = ""
White = ""
LightBlack = ""
LightRed = ""
LightGreen = ""
LightYellow = ""
LightBlue = ""
LightMagenta = ""
LightCyan = ""
LightWhite = ""
} else {
Black = ColorCode("black")
Red = ColorCode("red")
Green = ColorCode("green")
Yellow = ColorCode("yellow")
Blue = ColorCode("blue")
Magenta = ColorCode("magenta")
Cyan = ColorCode("cyan")
White = ColorCode("white")
LightBlack = ColorCode("black+h")
LightRed = ColorCode("red+h")
LightGreen = ColorCode("green+h")
LightYellow = ColorCode("yellow+h")
LightBlue = ColorCode("blue+h")
LightMagenta = ColorCode("magenta+h")
LightCyan = ColorCode("cyan+h")
LightWhite = ColorCode("white+h")
}
}

65
vendor/github.com/mgutz/ansi/doc.go generated vendored Normal file
View File

@@ -0,0 +1,65 @@
/*
Package ansi is a small, fast library to create ANSI colored strings and codes.
Installation
# this installs the color viewer and the package
go get -u github.com/mgutz/ansi/cmd/ansi-mgutz
Example
// colorize a string, SLOW
msg := ansi.Color("foo", "red+b:white")
// create a closure to avoid recalculating ANSI code compilation
phosphorize := ansi.ColorFunc("green+h:black")
msg = phosphorize("Bring back the 80s!")
msg2 := phospohorize("Look, I'm a CRT!")
// cache escape codes and build strings manually
lime := ansi.ColorCode("green+h:black")
reset := ansi.ColorCode("reset")
fmt.Println(lime, "Bring back the 80s!", reset)
Other examples
Color(s, "red") // red
Color(s, "red+b") // red bold
Color(s, "red+B") // red blinking
Color(s, "red+u") // red underline
Color(s, "red+bh") // red bold bright
Color(s, "red:white") // red on white
Color(s, "red+b:white+h") // red bold on white bright
Color(s, "red+B:white+h") // red blink on white bright
To view color combinations, from terminal
ansi-mgutz
Style format
"foregroundColor+attributes:backgroundColor+attributes"
Colors
black
red
green
yellow
blue
magenta
cyan
white
Attributes
b = bold foreground
B = Blink foreground
u = underline foreground
h = high intensity (bright) foreground, background
i = inverse
Wikipedia ANSI escape codes [Colors](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
*/
package ansi

57
vendor/github.com/mgutz/ansi/print.go generated vendored Normal file
View File

@@ -0,0 +1,57 @@
package ansi
import (
"fmt"
"sort"
colorable "github.com/mattn/go-colorable"
)
// PrintStyles prints all style combinations to the terminal.
func PrintStyles() {
// for compatibility with Windows, not needed for *nix
stdout := colorable.NewColorableStdout()
bgColors := []string{
"",
":black",
":red",
":green",
":yellow",
":blue",
":magenta",
":cyan",
":white",
}
keys := make([]string, 0, len(Colors))
for k := range Colors {
keys = append(keys, k)
}
sort.Sort(sort.StringSlice(keys))
for _, fg := range keys {
for _, bg := range bgColors {
fmt.Fprintln(stdout, padColor(fg, []string{"" + bg, "+b" + bg, "+bh" + bg, "+u" + bg}))
fmt.Fprintln(stdout, padColor(fg, []string{"+s" + bg, "+i" + bg}))
fmt.Fprintln(stdout, padColor(fg, []string{"+uh" + bg, "+B" + bg, "+Bb" + bg /* backgrounds */, "" + bg + "+h"}))
fmt.Fprintln(stdout, padColor(fg, []string{"+b" + bg + "+h", "+bh" + bg + "+h", "+u" + bg + "+h", "+uh" + bg + "+h"}))
}
}
}
func pad(s string, length int) string {
for len(s) < length {
s += " "
}
return s
}
func padColor(color string, styles []string) string {
buffer := ""
for _, style := range styles {
buffer += Color(pad(color+style, 20), color+style)
}
return buffer
}

24
vendor/vendor.json vendored
View File

@@ -14,6 +14,12 @@
"revision": "2c14e1cca90af49b3b21fe2d7aaa8cc7f9da2ff8",
"revisionTime": "2017-04-02T15:00:40Z"
},
{
"checksumSHA1": "zkENTbOfU8YoxPfFwVAhTz516Dg=",
"path": "github.com/dustin/go-humanize",
"revision": "7a41df006ff9af79a29f0ffa9c5f21fbe6314a2d",
"revisionTime": "2017-01-10T07:11:07Z"
},
{
"checksumSHA1": "3z3RDSBixjg5A0XPPwAfrpoajoQ=",
"path": "github.com/gliderlabs/ssh",
@@ -56,6 +62,18 @@
"revision": "1c35d901db3da928c72a72d8458480cc9ade058f",
"revisionTime": "2017-01-02T12:52:26Z"
},
{
"checksumSHA1": "6WOrEA4SH+M4UPESadwZ7J4ytnE=",
"path": "github.com/mattn/go-colorable",
"revision": "6fcc0c1fd9b620311d821b106a400b35dc95c497",
"revisionTime": "2017-11-11T06:59:53Z"
},
{
"checksumSHA1": "xZuhljnmBysJPta/lMyYmJdujCg=",
"path": "github.com/mattn/go-isatty",
"revision": "30a891c33c7cde7b02a981314b4228ec99380cca",
"revisionTime": "2016-11-23T14:36:37Z"
},
{
"checksumSHA1": "cJE7dphDlam/i7PhnsyosNWtbd4=",
"path": "github.com/mattn/go-runewidth",
@@ -68,6 +86,12 @@
"revision": "615c193e01d8d462eef7ee390171506f531a1c9a",
"revisionTime": "2017-10-24T02:57:10Z"
},
{
"checksumSHA1": "CIK3BBNX3nuUQCmNqTQydNfMNKI=",
"path": "github.com/mgutz/ansi",
"revision": "9520e82c474b0a04dd04f8a40959027271bab992",
"revisionTime": "2017-02-06T15:57:36Z"
},
{
"checksumSHA1": "14LZ4g2c1lkJyGBl7h18Ji/aW5c=",
"path": "github.com/moby/moby/pkg/namesgenerator",