├── .gitignore ├── media └── demo.gif ├── misc ├── run_ssh ├── sshd_config └── sshd ├── version.go ├── CHANGELOG.md ├── dockerfiles └── Dockerfile.ubuntu16 ├── go.mod ├── main.go ├── SSHDockerfile ├── goreleaser.yml ├── cli_test.go ├── README.md ├── tests └── pam_unittest.c ├── Makefile ├── pam └── pam.c ├── cli.go ├── go.sum └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | dist 3 | builds 4 | misc/secret.json 5 | -------------------------------------------------------------------------------- /media/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyama86/google-web-oauth/HEAD/media/demo.gif -------------------------------------------------------------------------------- /misc/run_ssh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rsyslogd & 3 | sleep 1 4 | tail -f /var/log/syslog & 5 | /usr/sbin/sshd -D 6 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const Name string = "google-web-oauth" 4 | const Version string = "0.1.0" 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 (2020-01-30) 2 | 3 | Initial release 4 | 5 | ### Added 6 | 7 | - Add Fundamental features 8 | 9 | ### Deprecated 10 | 11 | - Nothing 12 | 13 | ### Removed 14 | 15 | - Nothing 16 | 17 | ### Fixed 18 | 19 | - Nothing 20 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile.ubuntu16: -------------------------------------------------------------------------------- 1 | FROM ubuntu:xenial 2 | MAINTAINER pyama86 3 | 4 | RUN apt-get -qqy update && \ 5 | apt-get install -qqy glibc-source gcc make \ 6 | bzip2 unzip debhelper dh-make devscripts cdbs clang libpam0g-dev git 7 | -------------------------------------------------------------------------------- /misc/sshd_config: -------------------------------------------------------------------------------- 1 | KbdInteractiveAuthentication yes 2 | PasswordAuthentication no 3 | UsePAM yes 4 | X11Forwarding yes 5 | PrintMotd no 6 | Subsystem sftp /usr/lib/openssh/sftp-server 7 | PermitRootLogin yes 8 | PORT 22 9 | AuthenticationMethods publickey,keyboard-interactive 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pyama86/google-web-oauth 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/k0kubun/pp v3.0.1+incompatible 7 | github.com/mattn/go-colorable v0.1.4 // indirect 8 | github.com/sirupsen/logrus v1.4.2 9 | golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d 10 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d 11 | google.golang.org/api v0.15.0 12 | ) 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/syslog" 5 | "os" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func init() { 11 | logger, err := syslog.New(syslog.LOG_ERR|syslog.LOG_USER, "google-web-oauth") 12 | if err == nil { 13 | logrus.SetOutput(logger) 14 | } 15 | } 16 | func main() { 17 | cli := &CLI{outStream: os.Stdout, errStream: os.Stderr} 18 | os.Exit(cli.Run(os.Args)) 19 | } 20 | -------------------------------------------------------------------------------- /SSHDockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | RUN apt -qqy update && apt -qqy install openssh-server rsyslog 3 | RUN mkdir /var/run/sshd 4 | RUN echo 'root:test' | chpasswd 5 | COPY misc/sshd /etc/pam.d/sshd 6 | COPY misc/sshd_config /etc/ssh/sshd_config 7 | COPY misc/run_ssh /usr/local/bin/ 8 | ENV NOTVISIBLE "in users profile" 9 | RUN echo "export VISIBLE=now" >> /etc/profile 10 | EXPOSE 22 11 | 12 | CMD ["/bin/bash", "/usr/local/bin/run_ssh"] 13 | -------------------------------------------------------------------------------- /goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - id: google-web-oauth 3 | binary: google-web-oauth 4 | goos: 5 | - linux 6 | goarch: 7 | - amd64 8 | ldflags: 9 | -X main.Version={{.Version}} 10 | nfpms: 11 | - 12 | id: google-web-oauth-nfpms 13 | name_template: "{{ .ProjectName }}_{{ .Version }}-1_{{ .Arch }}" 14 | builds: 15 | - google-web-oauth 16 | homepage: https://github.com/pyama86/google-web-oauth 17 | maintainer: pyama86 18 | description: pam module by google oauth 19 | license: MIT 20 | formats: 21 | - deb 22 | - rpm 23 | bindir: /usr/bin 24 | epoch: 1 25 | -------------------------------------------------------------------------------- /misc/sshd: -------------------------------------------------------------------------------- 1 | auth required google-web-oauth.so 2 | #@include common-auth 3 | account required pam_nologin.so 4 | @include common-account 5 | session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close 6 | session optional pam_loginuid.so 7 | session optional pam_keyinit.so force revoke 8 | @include common-session 9 | session optional pam_motd.so motd=/run/motd.dynamic 10 | session optional pam_motd.so noupdate 11 | session optional pam_mail.so standard noenv 12 | session required pam_limits.so 13 | session required pam_env.so session required pam_env.so user_readenv=1 envfile=/etc/default/locale 14 | session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open 15 | @include common-password 16 | -------------------------------------------------------------------------------- /cli_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestRun_versionFlag(t *testing.T) { 11 | outStream, errStream := new(bytes.Buffer), new(bytes.Buffer) 12 | cli := &CLI{outStream: outStream, errStream: errStream} 13 | args := strings.Split("./google-web-oauth -version", " ") 14 | 15 | status := cli.Run(args) 16 | if status != ExitCodeOK { 17 | t.Errorf("expected %d to eq %d", status, ExitCodeOK) 18 | } 19 | 20 | expected := fmt.Sprintf("google-web-oauth version %s", Version) 21 | if !strings.Contains(errStream.String(), expected) { 22 | t.Errorf("expected %q to eq %q", errStream.String(), expected) 23 | } 24 | } 25 | 26 | func TestRun_configFlag(t *testing.T) { 27 | outStream, errStream := new(bytes.Buffer), new(bytes.Buffer) 28 | cli := &CLI{outStream: outStream, errStream: errStream} 29 | args := strings.Split("./google-web-oauth -config", " ") 30 | 31 | status := cli.Run(args) 32 | _ = status 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # google-web-oauth 2 | ## Description 3 | google-web-oauth is ssh authentication software. 4 | this provides you with multi-factor authentication. 5 | 6 | 7 | ![demo](https://github.com/pyama86/google-web-oauth/blob/master/media/demo.gif) 8 | ## Usage 9 | ### USE PAM 10 | for ubuntu 11 | 12 | 1. Get the oAuth client ID on google. 13 | 2. Please place the secret file to `/etc/google-web-oauth/client_secret.json` 14 | 3. set binary. 15 | - /lib/x86_64-linux-gnu/security/google-web-oauth.so 16 | - /usr/bin/google-web-oauth 17 | 4. Write the following in /etc/pam.d/sshd 18 | ``` 19 | auth required google-web-oauth.so 20 | #@include common-auth # must comment out. 21 | ``` 22 | 23 | 5. Write the following in sshd_config and restart sshd process. 24 | 25 | ``` 26 | KbdInteractiveAuthentication yes 27 | UsePAM yes 28 | AuthenticationMethods publickey,keyboard-interactive 29 | ChallengeResponseAuthentication yes 30 | ``` 31 | 32 | ### USE SSH 33 | 34 | > In this case, they skip ForceCommand when use ProxyCommand, it is vulnerable... 35 | 36 | 1. Get the oAuth client ID on google. 37 | 2. Please place the secret file to `/etc/google-web-oauth/client_secret.json` 38 | 3. set binary. 39 | - /usr/bin/google-web-oauth 40 | 4. Write the following in sshd_config and restart sshd process. 41 | 42 | ``` 43 | ForceCommand sudo SSH_CONNECTION="$SSH_CONNECTION" /usr/bin/google-web-oauth && eval ${SSH_ORIGINAL_COMMAND:-/bin/bash} 44 | ``` 45 | 46 | ## blog 47 | - [SSHログイン時に公開鍵認証とGoogle OAuthで多要素認証する](https://ten-snapon.com/archives/2306) 48 | 49 | ## Install 50 | 51 | To install, use `go get`: 52 | 53 | ```bash 54 | $ go get -d github.com/pyama86/google-web-oauth 55 | ``` 56 | 57 | ## Contribution 58 | 59 | 1. Fork ([https://github.com/pyama86/google-web-oauth/fork](https://github.com/pyama86/google-web-oauth/fork)) 60 | 1. Create a feature branch 61 | 1. Commit your changes 62 | 1. Rebase your local changes against the master branch 63 | 1. Run test suite with the `go test ./...` command and confirm that it passes 64 | 1. Run `gofmt -s` 65 | 1. Create a new Pull Request 66 | 67 | ## Author 68 | 69 | [pyama86](https://github.com/pyama86) 70 | -------------------------------------------------------------------------------- /tests/pam_unittest.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | static void *pam_module; 15 | 16 | static const char *get_error_msg(void) { 17 | const char *(*get_error_msg)(void) = 18 | (const char *(*)(void))dlsym(pam_module, "get_error_msg"); 19 | const char* msg = get_error_msg ? get_error_msg() : ""; 20 | const char* p = strrchr(msg, '\n'); 21 | if (p) { 22 | msg = p+1; 23 | } 24 | return msg; 25 | } 26 | 27 | static void print_diagnostics(int signo) { 28 | if (*get_error_msg()) { 29 | fprintf(stderr, "%s\n", get_error_msg()); 30 | } 31 | _exit(1); 32 | } 33 | 34 | static int converse(pam_handle_t *pamh, int nargs, const struct pam_message **message, 35 | struct pam_response **response) 36 | { 37 | struct pam_conv *conv; 38 | int retval = pam_get_item(pamh, PAM_CONV, (void *)&conv); 39 | if (retval != PAM_SUCCESS) { 40 | return retval; 41 | } 42 | return conv->conv(nargs, message, response, conv->appdata_ptr); 43 | } 44 | 45 | int pam_get_user(pam_handle_t *pamh, const char **user, const char *prompt) { 46 | return pam_get_item(pamh, PAM_USER, (void *)user); 47 | } 48 | 49 | int pam_get_item(const pam_handle_t *pamh, int item_type, const void **item) { 50 | switch (item_type) { 51 | case PAM_SERVICE: { 52 | static const char service[] = "google-web-oauth-pam-test"; 53 | *item = service; 54 | return PAM_SUCCESS; 55 | } 56 | case PAM_USER: { 57 | char *user = getenv("USER"); 58 | *item = user; 59 | return PAM_SUCCESS; 60 | } 61 | case PAM_CONV: { 62 | static struct pam_conv conv = { .conv = converse }, *p_conv = &conv; 63 | *item = p_conv; 64 | return PAM_SUCCESS; 65 | } 66 | default: 67 | return PAM_BAD_ITEM; 68 | } 69 | } 70 | 71 | int main(int argc, char *argv[]) { 72 | printf("Loading PAM module\n"); 73 | pam_module = dlopen("./builds/pam_google_web_oauth.so.2.0", RTLD_NOW | RTLD_GLOBAL); 74 | if (pam_module == NULL) { 75 | fprintf(stderr, "dlopen(): %s\n", dlerror()); 76 | exit(1); 77 | } 78 | 79 | signal(SIGABRT, print_diagnostics); 80 | 81 | int (*pam_sm_authenticate)(pam_handle_t *, int, int, const char **) = 82 | (int (*)(pam_handle_t *, int, int, const char **)) 83 | dlsym(pam_module, "pam_sm_authenticate"); 84 | 85 | assert(pam_sm_authenticate != NULL); 86 | 87 | printf("Done\n"); 88 | return 0; 89 | } 90 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := $(shell git tag -l --sort=-v:refname | head -1 | sed 's/v//g') 2 | REVISION := $(shell git rev-parse --short HEAD) 3 | INFO_COLOR=\033[1;34m 4 | RESET=\033[0m 5 | BOLD=\033[1m 6 | BUILD=builds 7 | 8 | ifeq ("$(shell uname)","Darwin") 9 | GO ?= go 10 | else 11 | GO ?= GO111MODULE=on /usr/local/go/bin/go 12 | endif 13 | 14 | CC=gcc 15 | CFLAGS=-Wall -Wstrict-prototypes -Werror -fPIC -std=c99 -D_GNU_SOURCE 16 | LD_SONAME=-Wl,-soname,pam_google_web_oauth.so.2 17 | LIBRARY=pam_google_web_oauth.so.2.0 18 | 19 | TEST ?= $(shell $(GO) list ./... | grep -v -e vendor -e keys -e tmp) 20 | 21 | .PHONY: clean 22 | 23 | clean: 24 | rm -rf ./builds 25 | 26 | ssh_container: 27 | docker build -f SSHDockerfile -t ssh . 28 | docker run --privileged \ 29 | -v `pwd`/builds/pam_google_web_oauth.so.2.0:/lib/x86_64-linux-gnu/security/google-web-oauth.so \ 30 | -v `pwd`/builds/google-web-oauth:/usr/bin/google-web-oauth \ 31 | -v `pwd`/misc/secret.json:/etc/google-web-oauth/client_secret.json \ 32 | -v $(HOME)/.ssh/id_rsa.pub:/root/.ssh/authorized_keys \ 33 | -p 10022:22 ssh 34 | 35 | linux_build: 36 | docker run -v `pwd`:/go/src/github.com/pyama86/google-web-oauth -v $(GOPATH):/go -w /go/src/github.com/pyama86/google-web-oauth golang:latest make build 37 | 38 | linux_pam_build: 39 | docker build -t pam -f dockerfiles/Dockerfile.ubuntu16 . 40 | docker run -v `pwd`:/go/src/github.com/pyama86/google-web-oauth -v $(GOPATH):/go -w /go/src/github.com/pyama86/google-web-oauth pam make pam_build 41 | 42 | build: 43 | $(GO) build -o builds/google-web-oauth -ldflags "-s -w -X main.Version=$(VERSION)-$(REVISION)" 44 | 45 | pam_build: clean 46 | mkdir $(BUILD) 47 | @echo "$(INFO_COLOR)==> $(RESET)$(BOLD)Building nss_stns$(RESET)" 48 | $(CC) $(CFLAGS) -c pam/pam.c -o $(BUILD)/pam.o 49 | $(CC) $(LDFLAGS) -shared $(LD_SONAME) -o builds/$(LIBRARY) \ 50 | $(BUILD)/pam.o 51 | 52 | pam_test: pam_build 53 | $(CC) -rdynamic -o builds/pam_unittest tests/pam_unittest.c -ldl 54 | ./builds/pam_unittest 55 | 56 | deps: 57 | go get -u golang.org/x/lint/golint 58 | 59 | git-semv: 60 | brew tap linyows/git-semv 61 | brew install git-semv 62 | 63 | ci: unit_test lint 64 | lint: deps 65 | @echo "$(INFO_COLOR)==> $(RESET)$(BOLD)Linting$(RESET)" 66 | golint -min_confidence 1.1 -set_exit_status $(TEST) 67 | 68 | unit_test: ## Run test 69 | @echo "$(INFO_COLOR)==> $(RESET)$(BOLD)Testing$(RESET)" 70 | $(GO) test -v $(TEST) -timeout=30s -parallel=4 -coverprofile cover.out.tmp 71 | cat cover.out.tmp | grep -v -e "main.go" -e "cmd.go" -e "_mock.go" > cover.out 72 | $(GO) tool cover -func cover.out 73 | $(GO) test -race $(TEST) 74 | 75 | linux_pam_test: 76 | docker build -t pam -f dockerfiles/Dockerfile.ubuntu16 . 77 | docker run -v `pwd`:/go/src/github.com/pyama86/google-web-oauth -v $(GOPATH):/go -w /go/src/github.com/pyama86/google-web-oauth pam make pam_test 78 | 79 | release: goreleaser 80 | git semv patch --bump 81 | goreleaser --rm-dist 82 | run: 83 | $(GO) run main.go version.go cli.go 84 | 85 | github_release: linux_build linux_pam_build 86 | git semv patch --bump 87 | ghr --replace v$(VERSION) builds/ 88 | 89 | docker: 90 | docker build -t pyama:google-web-oauth . 91 | -------------------------------------------------------------------------------- /pam/pam.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #ifdef sun 11 | #define PAM_CONST 12 | #else 13 | #define PAM_CONST const 14 | #endif 15 | #define MAXBUF 1024 16 | 17 | // source from: github.com/google/google-authenticator-libpam 18 | static int converse(pam_handle_t *pamh, int nargs, PAM_CONST struct pam_message **message, 19 | struct pam_response **response) 20 | { 21 | struct pam_conv *conv; 22 | int retval = pam_get_item(pamh, PAM_CONV, (void *)&conv); 23 | if (retval != PAM_SUCCESS) { 24 | return retval; 25 | } 26 | return conv->conv(nargs, message, response, conv->appdata_ptr); 27 | } 28 | static char *request_pass(pam_handle_t *pamh, int echocode, PAM_CONST char *prompt) 29 | { 30 | // Query user for verification code 31 | PAM_CONST struct pam_message msg = {.msg_style = echocode, .msg = prompt}; 32 | PAM_CONST struct pam_message *msgs = &msg; 33 | struct pam_response *resp = NULL; 34 | int retval = converse(pamh, 1, &msgs, &resp); 35 | char *ret = NULL; 36 | if (retval != PAM_SUCCESS || resp == NULL || resp->resp == NULL || *resp->resp == '\000') { 37 | if (retval == PAM_SUCCESS && resp && resp->resp) { 38 | ret = resp->resp; 39 | } 40 | } else { 41 | ret = resp->resp; 42 | } 43 | 44 | // Deallocate temporary storage 45 | if (resp) { 46 | if (!ret) { 47 | free(resp->resp); 48 | } 49 | free(resp); 50 | } 51 | 52 | return ret; 53 | } 54 | 55 | int exec_cmd(char *user_env, char *from_env, char *argv[]) 56 | { 57 | char *envp[] = {user_env, from_env, NULL}; 58 | pid_t pid = fork(); 59 | if (pid == -1) { 60 | return PAM_AUTHINFO_UNAVAIL; 61 | } else if (pid == 0) { 62 | execve(argv[0], argv, envp); 63 | exit(-1); 64 | } else { 65 | int status; 66 | if (wait(&status) == -1) { 67 | return PAM_AUTHINFO_UNAVAIL; 68 | } else if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { 69 | return PAM_SUCCESS; 70 | } 71 | } 72 | return PAM_AUTHINFO_UNAVAIL; 73 | } 74 | 75 | int popen_cmd(char *user_env, char *from_env, char *cmd, char *arg, char *res) 76 | { 77 | FILE *fp; 78 | char *c; 79 | char buf[MAXBUF]; 80 | putenv(user_env); 81 | putenv(from_env); 82 | 83 | if (arg != NULL) { 84 | c = malloc(strlen(cmd) + strlen(arg) + 2); 85 | sprintf(c, "%s %s", cmd, arg); 86 | } else { 87 | c = cmd; 88 | } 89 | 90 | if ((fp = popen(c, "r")) == NULL) { 91 | goto err; 92 | } 93 | 94 | int total_len = 0; 95 | int len = 0; 96 | 97 | while (fgets(buf, sizeof(buf), fp) != NULL) { 98 | len = strlen(buf); 99 | strcpy(res + total_len, buf); 100 | total_len += len; 101 | } 102 | 103 | if (arg != NULL) 104 | free(c); 105 | 106 | return pclose(fp); 107 | err: 108 | if (arg != NULL) 109 | free(c); 110 | return 1; 111 | } 112 | 113 | int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) 114 | { 115 | char res[MAXBUF]; 116 | const char *user; 117 | const char *from; 118 | char user_env[MAXBUF], from_env[MAXBUF]; 119 | 120 | if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS || user == NULL || *user == '\0') { 121 | return PAM_USER_UNKNOWN; 122 | } 123 | 124 | if (pam_get_item(pamh, PAM_RHOST, (void *)&from) != PAM_SUCCESS) { 125 | return PAM_ABORT; 126 | } 127 | 128 | snprintf(user_env, MAXBUF, "USER=%s", user); 129 | snprintf(from_env, MAXBUF, "SSH_CONNECTION=%s", from); 130 | 131 | int ret = popen_cmd(user_env, from_env, "/usr/bin/google-web-oauth", "-only-url", res); 132 | if (ret != 0) { 133 | return PAM_AUTHINFO_UNAVAIL; 134 | } 135 | 136 | if (strcmp(res, "auth ok with cache token\n") == 0) { 137 | // auth ok with cache token 138 | return PAM_SUCCESS; 139 | } 140 | char *code = NULL; 141 | code = request_pass(pamh, PAM_PROMPT_ECHO_OFF, res); 142 | if (!code) { 143 | return PAM_AUTHINFO_UNAVAIL; 144 | } 145 | 146 | char *arg[] = {"/usr/bin/google-web-oauth", "-code", code, NULL}; 147 | ret = exec_cmd(user_env, from_env, arg); 148 | if (ret != PAM_SUCCESS) { 149 | goto err; 150 | } 151 | 152 | return PAM_SUCCESS; 153 | err: 154 | return PAM_AUTHINFO_UNAVAIL; 155 | } 156 | 157 | int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) 158 | { 159 | return PAM_SUCCESS; 160 | } 161 | -------------------------------------------------------------------------------- /cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/url" 10 | "os" 11 | "os/user" 12 | "path/filepath" 13 | "strconv" 14 | "strings" 15 | "syscall" 16 | 17 | "github.com/sirupsen/logrus" 18 | "golang.org/x/crypto/ssh/terminal" 19 | "golang.org/x/oauth2" 20 | "golang.org/x/oauth2/google" 21 | oauthapi "google.golang.org/api/oauth2/v2" 22 | ) 23 | 24 | // Exit codes are int values that represent an exit code for a particular error. 25 | const ( 26 | ExitCodeOK int = 0 27 | ExitCodeError int = 1 + iota 28 | ) 29 | 30 | // CLI is the command line object 31 | type CLI struct { 32 | // outStream and errStream are the stdout and stderr 33 | // to write message from the CLI. 34 | outStream, errStream io.Writer 35 | } 36 | 37 | // Run invokes the CLI with the given arguments. 38 | func (cli *CLI) Run(args []string) int { 39 | var ( 40 | config string 41 | version bool 42 | onlyURL bool 43 | code string 44 | ) 45 | 46 | flags := flag.NewFlagSet(Name, flag.ContinueOnError) 47 | flags.SetOutput(cli.errStream) 48 | 49 | flags.StringVar(&config, "config", "/etc/google-web-oauth/client_secret.json", "Config file path") 50 | flags.StringVar(&config, "c", "/etc/google-web-oauth/client_secret.json", "Config file path(Short)") 51 | 52 | flags.BoolVar(&version, "version", false, "Print version information and quit.") 53 | 54 | flags.BoolVar(&onlyURL, "only-url", false, "only show url") 55 | flags.StringVar(&code, "code", "", "auth code from web") 56 | 57 | // Parse commandline flag 58 | if err := flags.Parse(args[1:]); err != nil { 59 | return ExitCodeError 60 | } 61 | 62 | // Show version 63 | if version { 64 | fmt.Fprintf(cli.errStream, "%s version %s\n", Name, Version) 65 | return ExitCodeOK 66 | } 67 | if err := cli.run(config, onlyURL, []byte(code)); err != nil { 68 | logrus.Error(err) 69 | return ExitCodeError 70 | } 71 | return ExitCodeOK 72 | 73 | } 74 | func (cli *CLI) run(config string, onlyURL bool, code []byte) error { 75 | b, err := ioutil.ReadFile(config) 76 | if err != nil { 77 | return fmt.Errorf("Unable to read client secret file: %s", config) 78 | } 79 | 80 | c, err := google.ConfigFromJSON(b, "profile") 81 | if err != nil { 82 | return fmt.Errorf("Unable to parse client secret file to config: %v", err) 83 | } 84 | 85 | cacheFile, err := tokenCacheFile() 86 | if err != nil { 87 | return err 88 | } 89 | 90 | tok, err := tokenFromFile(cacheFile) 91 | if err != nil { 92 | goto web 93 | } 94 | 95 | if tok.OAuthToken == nil || tok.LastIP != lastIP() { 96 | goto web 97 | } else { 98 | client := oauth2.NewClient(oauth2.NoContext, c.TokenSource(oauth2.NoContext, tok.OAuthToken)) 99 | svr, err := oauthapi.New(client) 100 | if err != nil { 101 | goto web 102 | } 103 | 104 | _, err = svr.Userinfo.Get().Do() 105 | if err != nil { 106 | goto web 107 | } 108 | fmt.Println("auth ok with cache token") 109 | } 110 | 111 | return nil 112 | web: 113 | return getTokenFromWebAndSaveFile(c, cacheFile, onlyURL, code) 114 | } 115 | 116 | func getTokenFromWebAndSaveFile(c *oauth2.Config, cacheFile string, onlyURL bool, code []byte) error { 117 | var err error 118 | if code == nil || len(code) == 0 { 119 | authURL := c.AuthCodeURL("state-token", oauth2.AccessTypeOffline) 120 | fmt.Printf("Go to the following link in your browser then type the "+ 121 | "authorization code: \n\n%v\n\nPlease type code:", authURL) 122 | 123 | if onlyURL { 124 | return nil 125 | } 126 | 127 | code, err = terminal.ReadPassword(int(syscall.Stdin)) 128 | if err != nil { 129 | return fmt.Errorf("Unable to read authorization code %v", err) 130 | } 131 | } 132 | 133 | tok, err := c.Exchange(oauth2.NoContext, string(code)) 134 | if err != nil { 135 | return fmt.Errorf("Unable to retrieve token from web %v", err) 136 | } 137 | return saveToken(cacheFile, tok) 138 | } 139 | 140 | func tokenCacheFile() (string, error) { 141 | uname := os.Getenv("USER") 142 | if os.Getenv("SUDO_USER") != "" { 143 | uname = os.Getenv("SUDO_USER") 144 | } 145 | userInfo, err := user.Lookup(uname) 146 | if err != nil { 147 | return "", fmt.Errorf("user lookup error %s %s", uname, err.Error()) 148 | } 149 | 150 | // create home dir 151 | if err := createDir(userInfo.HomeDir, userInfo.Uid, userInfo.Gid, 0755); err != nil { 152 | return "", err 153 | } 154 | // create token dir 155 | tokenCacheDir := filepath.Join("/opt/google-web-oauth", fmt.Sprintf("%s.json", uname)) 156 | if err := createDir(tokenCacheDir, "0", "0", 0700); err != nil { 157 | return "", err 158 | } 159 | 160 | return filepath.Join(tokenCacheDir, url.QueryEscape("google_oauth.json")), nil 161 | } 162 | 163 | func createDir(path, uid, gid string, mode os.FileMode) error { 164 | if _, err := os.Stat(path); os.IsNotExist(err) { 165 | if err = os.MkdirAll(path, mode); err != nil { 166 | return err 167 | } 168 | iuid, err := strconv.Atoi(uid) 169 | if err != nil { 170 | return err 171 | } 172 | igid, err := strconv.Atoi(gid) 173 | if err != nil { 174 | return err 175 | } 176 | if err = os.Chown(path, iuid, igid); err != nil { 177 | return err 178 | } 179 | } 180 | return nil 181 | 182 | } 183 | 184 | type tokenCache struct { 185 | OAuthToken *oauth2.Token 186 | LastIP string 187 | } 188 | 189 | func tokenFromFile(file string) (*tokenCache, error) { 190 | f, err := os.Open(file) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | tc := &tokenCache{} 196 | err = json.NewDecoder(f).Decode(tc) 197 | defer f.Close() 198 | return tc, err 199 | } 200 | 201 | func saveToken(file string, token *oauth2.Token) error { 202 | f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 203 | if err != nil { 204 | return err 205 | } 206 | 207 | tc := tokenCache{ 208 | OAuthToken: token, 209 | LastIP: lastIP(), 210 | } 211 | defer f.Close() 212 | return json.NewEncoder(f).Encode(tc) 213 | } 214 | func lastIP() string { 215 | return strings.Split(os.Getenv("SSH_CONNECTION"), " ")[0] 216 | } 217 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= 5 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 6 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 7 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 11 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 12 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 13 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 14 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 15 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 16 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 17 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 18 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 19 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 20 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 21 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 22 | github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= 23 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 24 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 25 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 26 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 27 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 28 | github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= 29 | github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= 30 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 31 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 32 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 33 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 34 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 38 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 39 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 40 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 41 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 42 | go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= 43 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 44 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 45 | golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U= 46 | golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 47 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 48 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 49 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 50 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 51 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 52 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 53 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 54 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 55 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= 56 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 57 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 58 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 59 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 60 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 61 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= 62 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 63 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 64 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 65 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 66 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 67 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 68 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 69 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 72 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 73 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 74 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 75 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 76 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 78 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 79 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= 80 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 82 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 83 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 84 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 85 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 86 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 87 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 88 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 89 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 90 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 91 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 92 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 93 | google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA= 94 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 95 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 96 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 97 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 98 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 99 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 100 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 101 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= 102 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 103 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 104 | google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= 105 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 106 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 107 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 108 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------