diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..afbc551 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +vendor +Dockerfile \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5f53ae0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM ubuntu:latest + +# Install Go +RUN apt-get update && apt-get install -y wget git gcc unzip +RUN wget -q -P /tmp https://dl.google.com/go/go1.11.2.linux-amd64.tar.gz +RUN tar -C /usr/local -xzf /tmp/go1.11.2.linux-amd64.tar.gz +RUN rm /tmp/go1.11.2.linux-amd64.tar.gz +ENV GOPATH /go +ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH +RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH" + +# Setup work directory +COPY . /go/src/github.com/vwxyzjn/portwarden +WORKDIR /go/src/github.com/vwxyzjn/portwarden + +# Install Go Dep +RUN wget -q https://github.com/golang/dep/releases/download/v0.4.1/dep-linux-amd64 +RUN mv dep-linux-amd64 /usr/bin/dep +RUN chmod +x /usr/bin/dep + +# Install Bitwarden CLI +RUN wget -q https://github.com/bitwarden/cli/releases/download/v1.6.0/bw-linux-1.6.0.zip +RUN unzip bw-linux-1.6.0.zip -d /usr/bin/ +RUN chmod +x /usr/bin/bw + +# Run dep +# Notice git is the dependency for running dep +RUN cd /go/src/github.com/vwxyzjn/portwarden && dep ensure --vendor-only + +# Ready to run +EXPOSE 5000 diff --git a/Dockerfile.Build b/Dockerfile.Build new file mode 100644 index 0000000..786309f --- /dev/null +++ b/Dockerfile.Build @@ -0,0 +1,46 @@ +FROM ubuntu:latest + +# Install Go +RUN apt-get update && apt-get install -y wget git gcc unzip +RUN wget -q -P /tmp https://dl.google.com/go/go1.11.2.linux-amd64.tar.gz +RUN tar -C /usr/local -xzf /tmp/go1.11.2.linux-amd64.tar.gz +RUN rm /tmp/go1.11.2.linux-amd64.tar.gz +ENV GOPATH /go +ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH +RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH" + +# Setup work directory +COPY . /go/src/github.com/vwxyzjn/portwarden +WORKDIR /go/src/github.com/vwxyzjn/portwarden + +# Install Go Dep +RUN wget -q https://github.com/golang/dep/releases/download/v0.4.1/dep-linux-amd64 +RUN mv dep-linux-amd64 /usr/bin/dep +RUN chmod +x /usr/bin/dep + +# Install Bitwarden CLI +RUN wget -q https://github.com/bitwarden/cli/releases/download/v1.6.0/bw-linux-1.6.0.zip +RUN unzip bw-linux-1.6.0.zip -d /usr/bin/ +RUN chmod +x /usr/bin/bw + +# Run dep +# Notice git is the dependency for running dep +RUN cd /go/src/github.com/vwxyzjn/portwarden && dep ensure --vendor-only + +RUN go build /go/src/github.com/vwxyzjn/portwarden/web/worker/main.go && mv ./main /worker +RUN go build /go/src/github.com/vwxyzjn/portwarden/web/scheduler/main.go && mv ./main /scheduler + +# Ready to run +EXPOSE 5000 + + +FROM debian:stretch-20181112 +RUN apt-get update && apt-get install -y ca-certificates openssl +COPY --from=0 /usr/bin/bw /usr/bin/bw +COPY --from=0 /scheduler /go/src/github.com/vwxyzjn/portwarden/web/scheduler/scheduler +COPY --from=0 /worker /go/src/github.com/vwxyzjn/portwarden/web/worker/worker +COPY --from=0 /go/src/github.com/vwxyzjn/portwarden/web/portwardenCredentials.json /go/src/github.com/vwxyzjn/portwarden/web/portwardenCredentials.json +RUN chmod +x /go/src/github.com/vwxyzjn/portwarden/web/scheduler/scheduler +RUN chmod +x /go/src/github.com/vwxyzjn/portwarden/web/worker/worker +WORKDIR /go/src/github.com/vwxyzjn/portwarden +EXPOSE 5000 diff --git a/Gopkg.lock b/Gopkg.lock index f48ea6a..ce80e4e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -3,15 +3,107 @@ [[projects]] name = "cloud.google.com/go" - packages = ["compute/metadata"] + packages = [ + "compute/metadata", + "iam", + "internal/optional", + "internal/version", + "pubsub", + "pubsub/apiv1", + "pubsub/internal/distribution" + ] revision = "74b12019e2aa53ec27882158f59192d7cd6d1998" version = "v0.33.1" [[projects]] - name = "github.com/antonholmquist/jason" + branch = "master" + name = "github.com/RichardKnop/logging" packages = ["."] - revision = "c23cef7eaa75a6a5b8810120e167bd590d8fd2ab" - version = "v1.0.0" + revision = "b1d5d44c82d6c9fb6c8e8dad28412163586fd8ac" + +[[projects]] + name = "github.com/RichardKnop/machinery" + packages = [ + "v1", + "v1/backends/amqp", + "v1/backends/dynamodb", + "v1/backends/eager", + "v1/backends/iface", + "v1/backends/memcache", + "v1/backends/mongo", + "v1/backends/redis", + "v1/backends/result", + "v1/brokers/amqp", + "v1/brokers/eager", + "v1/brokers/errs", + "v1/brokers/gcppubsub", + "v1/brokers/iface", + "v1/brokers/redis", + "v1/brokers/sqs", + "v1/common", + "v1/config", + "v1/log", + "v1/retry", + "v1/tasks", + "v1/tracing" + ] + revision = "21f6dd0def9983b08c773554c89f64b0176da3a2" + version = "v1.5.3" + +[[projects]] + name = "github.com/RichardKnop/redsync" + packages = ["."] + revision = "54b27db64c180751e7049321a2e109ff220ccdcc" + version = "v1.2.0" + +[[projects]] + name = "github.com/aws/aws-sdk-go" + packages = [ + "aws", + "aws/awserr", + "aws/awsutil", + "aws/client", + "aws/client/metadata", + "aws/corehandlers", + "aws/credentials", + "aws/credentials/ec2rolecreds", + "aws/credentials/endpointcreds", + "aws/credentials/stscreds", + "aws/crr", + "aws/csm", + "aws/defaults", + "aws/ec2metadata", + "aws/endpoints", + "aws/request", + "aws/session", + "aws/signer/v4", + "internal/ini", + "internal/sdkio", + "internal/sdkrand", + "internal/sdkuri", + "internal/shareddefaults", + "private/protocol", + "private/protocol/json/jsonutil", + "private/protocol/jsonrpc", + "private/protocol/query", + "private/protocol/query/queryutil", + "private/protocol/rest", + "private/protocol/xml/xmlutil", + "service/dynamodb", + "service/dynamodb/dynamodbattribute", + "service/dynamodb/dynamodbiface", + "service/sqs", + "service/sqs/sqsiface", + "service/sts" + ] + revision = "08df30d135d32f1eb21bb7754eb042aeca521a4b" + version = "v1.15.84" + +[[projects]] + branch = "master" + name = "github.com/bradfitz/gomemcache" + packages = ["memcache"] + revision = "bc664df9673713a0ccf26e3b55a673ec7301088b" [[projects]] name = "github.com/davecgh/go-spew" @@ -32,12 +124,6 @@ ] revision = "cc9eb1d7ad760af14e8f918698f745e80377af4f" -[[projects]] - name = "github.com/gin-contrib/cors" - packages = ["."] - revision = "cf4846e6a636a76237a28d9286f163c132e841bc" - version = "v1.2" - [[projects]] branch = "master" name = "github.com/gin-contrib/sse" @@ -55,9 +141,32 @@ revision = "b869fe1415e4b9eb52f247441830d502aece2d4d" version = "v1.3.0" +[[projects]] + name = "github.com/go-redis/redis" + packages = [ + ".", + "internal", + "internal/consistenthash", + "internal/hashtag", + "internal/pool", + "internal/proto", + "internal/singleflight", + "internal/util" + ] + revision = "b3d9bf10f6666b2ee5100a6f3f84f4caf3b4e37d" + version = "v6.14.2" + [[projects]] name = "github.com/golang/protobuf" - packages = ["proto"] + packages = [ + "proto", + "protoc-gen-go/descriptor", + "ptypes", + "ptypes/any", + "ptypes/duration", + "ptypes/empty", + "ptypes/timestamp" + ] revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5" version = "v1.2.0" @@ -67,12 +176,50 @@ packages = ["."] revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a" +[[projects]] + name = "github.com/gomodule/redigo" + packages = [ + "internal", + "redis" + ] + revision = "9c11da706d9b7902c6da69c592f75637793fe121" + version = "v2.0.0" + +[[projects]] + name = "github.com/google/uuid" + packages = ["."] + revision = "9b3b1e0f5f99ae461456d768e7d301a7acdaa2d8" + version = "v1.1.0" + +[[projects]] + name = "github.com/googleapis/gax-go" + packages = ["."] + revision = "b001040cd31805261cbd978842099e326dfa857b" + version = "v2.0.2" + +[[projects]] + name = "github.com/imdario/mergo" + packages = ["."] + revision = "9f23e2d6bd2a77f959b2bf6acdbefd708a83a4a4" + version = "v0.3.6" + +[[projects]] + name = "github.com/jmespath/go-jmespath" + packages = ["."] + revision = "0b12d6b5" + [[projects]] name = "github.com/json-iterator/go" packages = ["."] revision = "1624edc4454b8682399def8740d46db5e4362ba4" version = "v1.1.5" +[[projects]] + name = "github.com/kelseyhightower/envconfig" + packages = ["."] + revision = "f611eb38b3875cc3bd991ca91c51d06446afa14c" + version = "v1.3.0" + [[projects]] name = "github.com/mattn/go-isatty" packages = ["."] @@ -103,6 +250,16 @@ revision = "cc3e4b2381762c56dbcdf9f93be03349a1dc1c14" version = "v1.0.0" +[[projects]] + name = "github.com/opentracing/opentracing-go" + packages = [ + ".", + "ext", + "log" + ] + revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38" + version = "v1.0.2" + [[projects]] name = "github.com/pierrec/lz4" packages = [ @@ -112,6 +269,12 @@ revision = "635575b42742856941dbc767b44905bb9ba083f6" version = "v2.0.7" +[[projects]] + branch = "master" + name = "github.com/streadway/amqp" + packages = ["."] + revision = "27835f1a64e97101d95306211f03c0620ffa295d" + [[projects]] branch = "master" name = "github.com/tidwall/pretty" @@ -135,6 +298,28 @@ revision = "0c6b41e72360850ca4f98dc341fd999726ea007f" version = "v0.5.4" +[[projects]] + name = "go.opencensus.io" + packages = [ + ".", + "exemplar", + "internal", + "internal/tagencoding", + "plugin/ocgrpc", + "plugin/ochttp", + "plugin/ochttp/propagation/b3", + "stats", + "stats/internal", + "stats/view", + "tag", + "trace", + "trace/internal", + "trace/propagation", + "trace/tracestate" + ] + revision = "b7bf3cdb64150a8c8c53b769fdeb2ba581bd4d4b" + version = "v0.18.0" + [[projects]] branch = "master" name = "golang.org/x/crypto" @@ -146,7 +331,13 @@ name = "golang.org/x/net" packages = [ "context", - "context/ctxhttp" + "context/ctxhttp", + "http/httpguts", + "http2", + "http2/hpack", + "idna", + "internal/timeseries", + "trace" ] revision = "adae6a3d119ae4890b46832a2e88a95adc62b8e7" @@ -162,21 +353,59 @@ ] revision = "f42d05182288abf10faef86d16c0d07b8d40ea2d" +[[projects]] + branch = "master" + name = "golang.org/x/sync" + packages = [ + "errgroup", + "semaphore" + ] + revision = "42b317875d0fa942474b76e1b46a6060d720ae6e" + [[projects]] branch = "master" name = "golang.org/x/sys" packages = ["unix"] revision = "66b7b1311ac80bbafcd2daeef9a5e6e2cd1e2399" +[[projects]] + name = "golang.org/x/text" + packages = [ + "collate", + "collate/build", + "internal/colltab", + "internal/gen", + "internal/tag", + "internal/triegen", + "internal/ucd", + "language", + "secure/bidirule", + "transform", + "unicode/bidi", + "unicode/cldr", + "unicode/norm", + "unicode/rangetable" + ] + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" + [[projects]] branch = "master" name = "google.golang.org/api" packages = [ "drive/v2", - "drive/v3", "gensupport", "googleapi", - "googleapi/internal/uritemplates" + "googleapi/internal/uritemplates", + "googleapi/transport", + "internal", + "iterator", + "option", + "support/bundler", + "transport", + "transport/grpc", + "transport/http", + "transport/http/internal/propagation" ] revision = "83a9d304b1e613fc253e1e2710778642fe81af53" @@ -191,18 +420,78 @@ "internal/log", "internal/modules", "internal/remote_api", + "internal/socket", "internal/urlfetch", + "socket", "urlfetch" ] revision = "4a4468ece617fc8205e99368fa2200e9d1fad421" version = "v1.3.0" +[[projects]] + branch = "master" + name = "google.golang.org/genproto" + packages = [ + "googleapis/api/annotations", + "googleapis/iam/v1", + "googleapis/pubsub/v1", + "googleapis/rpc/status", + "protobuf/field_mask" + ] + revision = "31ac5d88444a9e7ad18077db9a165d793ad06a2e" + +[[projects]] + name = "google.golang.org/grpc" + packages = [ + ".", + "balancer", + "balancer/base", + "balancer/roundrobin", + "codes", + "connectivity", + "credentials", + "credentials/oauth", + "encoding", + "encoding/proto", + "grpclog", + "internal", + "internal/backoff", + "internal/channelz", + "internal/envconfig", + "internal/grpcrand", + "internal/transport", + "keepalive", + "metadata", + "naming", + "peer", + "resolver", + "resolver/dns", + "resolver/passthrough", + "stats", + "status", + "tap" + ] + revision = "2e463a05d100327ca47ac218281906921038fd95" + version = "v1.16.0" + [[projects]] name = "gopkg.in/go-playground/validator.v8" packages = ["."] revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf" version = "v8.18.2" +[[projects]] + branch = "v2" + name = "gopkg.in/mgo.v2" + packages = [ + ".", + "bson", + "internal/json", + "internal/sasl", + "internal/scram" + ] + revision = "9856a29383ce1c59f308dd1cf0363a79b5bef6b5" + [[projects]] name = "gopkg.in/urfave/cli.v1" packages = ["."] @@ -218,6 +507,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "e94795c347f48a9ee90b68d79ae43762c0504d74ca19276f92b7c394faeab698" + inputs-digest = "b0982d243cf6580588055e8aa56320744ba316df6e3a49ca69fd19ec8211300c" solver-name = "gps-cdcl" solver-version = 1 diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..b72bb80 --- /dev/null +++ b/build.sh @@ -0,0 +1,7 @@ +## The folloiwng commands should be executed separately. + +docker build -t vwxyzjn/portwarden-scheduler:0.0.1 . + +# minikube docker-env + +docker-compose up \ No newline at end of file diff --git a/core.go b/core.go index 2a6ef3b..3ec9294 100644 --- a/core.go +++ b/core.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "os" "os/exec" + "path/filepath" "regexp" "strconv" "strings" @@ -50,6 +51,15 @@ type LoginCredentials struct { Code string `json:"code"` } +func CreateBackupBytesUsingBitwardenLocalJSON(dataJson []byte, BITWARDENCLI_APPDATA_DIR, passphrase, sessionKey string, sleepMilliseconds int) ([]byte, error) { + // Put data.json in the BITWARDENCLI_APPDATA_DIR + defer BWLogout() + if err := ioutil.WriteFile(filepath.Join(BITWARDENCLI_APPDATA_DIR, "data.json"), dataJson, 0644); err != nil { + return nil, err + } + return CreateBackupBytes(passphrase, sessionKey, sleepMilliseconds) +} + func CreateBackupFile(fileName, passphrase, sessionKey string, sleepMilliseconds int) error { defer BWLogout() if !strings.HasSuffix(fileName, ".portwarden") { @@ -195,6 +205,24 @@ func BWLoginGetSessionKey(lc *LoginCredentials) (string, error) { return sessionKey, nil } +func BWLoginGetSessionKeyAndDataJSON(lc *LoginCredentials, BITWARDENCLI_APPDATA_DIR string) (string, []byte, error) { + defer BWLogout() + sessionKey, err := BWLoginGetSessionKey(lc) + if err != nil { + return "", nil, err + } + dataJSONPath := filepath.Join(BITWARDENCLI_APPDATA_DIR, "data.json") + dat, err := ioutil.ReadFile(dataJSONPath) + if err != nil { + return "", nil, err + } + err = os.Remove(dataJSONPath) + if err != nil { + return "", nil, err + } + return sessionKey, dat, nil +} + func BWLogout() error { cmd := exec.Command("bw", "logout") return cmd.Run() diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..d4858df --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,51 @@ +version: '3' + +services: + + scheduler: + image: vwxyzjn/portwarden-base:1.1.0 + stdin_open: true + tty: true + environment: + - BITWARDENCLI_APPDATA_DIR=/BitwardenCLI + depends_on: + - redis + ports: + - 5000:5000 + volumes: + - .:/go/src/github.com/vwxyzjn/portwarden + working_dir: + /go/src/github.com/vwxyzjn/portwarden/web/scheduler + # command: + # go run main.go + + redis: + image: redis + ports: + - 6379:6379 + + worker: + image: vwxyzjn/portwarden-base:1.1.0 + stdin_open: true + tty: true + environment: + - BITWARDENCLI_APPDATA_DIR=/BitwardenCLI + depends_on: + - redis + deploy: + mode: replicated + replicas: 2 + volumes: + - .:/go/src/github.com/vwxyzjn/portwarden + working_dir: + /go/src/github.com/vwxyzjn/portwarden/web/worker + command: + go run main.go + + redis-commander: + image: rediscommander/redis-commander:latest + restart: always + environment: + - REDIS_HOSTS=local:redis:6379 + ports: + - "8081:8081" diff --git a/k8s/docker-compose.build.yaml b/k8s/docker-compose.build.yaml new file mode 100644 index 0000000..ca95822 --- /dev/null +++ b/k8s/docker-compose.build.yaml @@ -0,0 +1,53 @@ +version: '3' + +services: + + scheduler: + image: vwxyzjn/portwarden-base:1.6.0 + stdin_open: true + tty: true + environment: + - BITWARDENCLI_APPDATA_DIR=/BitwardenCLI + depends_on: + - redis + ports: + - 5000:5000 + working_dir: + /go/src/github.com/vwxyzjn/portwarden/web/scheduler + command: + ./scheduler + labels: + kompose.service.expose: "true" + kompose.service.type: "loadbalancer" + + redis: + image: redis + ports: + - 6379:6379 + + worker: + image: vwxyzjn/portwarden-base:1.6.0 + stdin_open: true + tty: true + environment: + - BITWARDENCLI_APPDATA_DIR=/BitwardenCLI + depends_on: + - redis + deploy: + mode: replicated + replicas: 2 + working_dir: + /go/src/github.com/vwxyzjn/portwarden/web/worker + command: + ./worker + + redis-commander: + image: rediscommander/redis-commander:latest + restart: always + environment: + - REDIS_HOSTS=local:redis:6379 + ports: + - "8081:8081" + labels: + kompose.service.expose: "true" + kompose.service.type: "loadbalancer" diff --git a/k8s/kompose/redis-commander-deployment.yaml b/k8s/kompose/redis-commander-deployment.yaml new file mode 100644 index 0000000..3a3fce4 --- /dev/null +++ b/k8s/kompose/redis-commander-deployment.yaml @@ -0,0 +1,32 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + annotations: + kompose.cmd: C:\Go\bin\kompose.exe convert -f docker-compose.build.yaml + kompose.service.expose: "true" + kompose.service.type: loadbalancer + kompose.version: 1.17.0 (a74acad) + creationTimestamp: null + labels: + io.kompose.service: redis-commander + name: redis-commander +spec: + replicas: 1 + strategy: {} + template: + metadata: + creationTimestamp: null + labels: + io.kompose.service: redis-commander + spec: + containers: + - env: + - name: REDIS_HOSTS + value: local:redis:6379 + image: rediscommander/redis-commander:latest + name: redis-commander + ports: + - containerPort: 8081 + resources: {} + restartPolicy: Always +status: {} diff --git a/k8s/kompose/redis-commander-ingress.yaml b/k8s/kompose/redis-commander-ingress.yaml new file mode 100644 index 0000000..44511eb --- /dev/null +++ b/k8s/kompose/redis-commander-ingress.yaml @@ -0,0 +1,16 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + creationTimestamp: null + labels: + io.kompose.service: redis-commander + name: redis-commander +spec: + rules: + - http: + paths: + - backend: + serviceName: redis-commander + servicePort: 8081 +status: + loadBalancer: {} diff --git a/k8s/kompose/redis-commander-service.yaml b/k8s/kompose/redis-commander-service.yaml new file mode 100644 index 0000000..ce8d0da --- /dev/null +++ b/k8s/kompose/redis-commander-service.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: C:\Go\bin\kompose.exe convert -f docker-compose.build.yaml + kompose.service.expose: "true" + kompose.service.type: loadbalancer + kompose.version: 1.17.0 (a74acad) + creationTimestamp: null + labels: + io.kompose.service: redis-commander + name: redis-commander +spec: + ports: + - name: "8081" + port: 8081 + targetPort: 8081 + selector: + io.kompose.service: redis-commander + type: LoadBalancer +status: + loadBalancer: {} diff --git a/k8s/kompose/redis-deployment.yaml b/k8s/kompose/redis-deployment.yaml new file mode 100644 index 0000000..772c583 --- /dev/null +++ b/k8s/kompose/redis-deployment.yaml @@ -0,0 +1,27 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + annotations: + kompose.cmd: C:\Go\bin\kompose.exe convert -f docker-compose.build.yaml + kompose.version: 1.17.0 (a74acad) + creationTimestamp: null + labels: + io.kompose.service: redis + name: redis +spec: + replicas: 1 + strategy: {} + template: + metadata: + creationTimestamp: null + labels: + io.kompose.service: redis + spec: + containers: + - image: redis + name: redis + ports: + - containerPort: 6379 + resources: {} + restartPolicy: Always +status: {} diff --git a/k8s/kompose/redis-service.yaml b/k8s/kompose/redis-service.yaml new file mode 100644 index 0000000..9ded402 --- /dev/null +++ b/k8s/kompose/redis-service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: C:\Go\bin\kompose.exe convert -f docker-compose.build.yaml + kompose.version: 1.17.0 (a74acad) + creationTimestamp: null + labels: + io.kompose.service: redis + name: redis +spec: + ports: + - name: "6379" + port: 6379 + targetPort: 6379 + selector: + io.kompose.service: redis +status: + loadBalancer: {} diff --git a/k8s/kompose/scheduler-deployment.yaml b/k8s/kompose/scheduler-deployment.yaml new file mode 100644 index 0000000..7ba9fba --- /dev/null +++ b/k8s/kompose/scheduler-deployment.yaml @@ -0,0 +1,37 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + annotations: + kompose.cmd: C:\Go\bin\kompose.exe convert -f docker-compose.build.yaml + kompose.service.expose: "true" + kompose.service.type: loadbalancer + kompose.version: 1.17.0 (a74acad) + creationTimestamp: null + labels: + io.kompose.service: scheduler + name: scheduler +spec: + replicas: 1 + strategy: {} + template: + metadata: + creationTimestamp: null + labels: + io.kompose.service: scheduler + spec: + containers: + - args: + - ./scheduler + env: + - name: BITWARDENCLI_APPDATA_DIR + value: /BitwardenCLI + image: vwxyzjn/portwarden-base:1.6.0 + name: scheduler + ports: + - containerPort: 5000 + resources: {} + stdin: true + tty: true + workingDir: /go/src/github.com/vwxyzjn/portwarden/web/scheduler + restartPolicy: Always +status: {} diff --git a/k8s/kompose/scheduler-ingress.yaml b/k8s/kompose/scheduler-ingress.yaml new file mode 100644 index 0000000..ab56abc --- /dev/null +++ b/k8s/kompose/scheduler-ingress.yaml @@ -0,0 +1,16 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + creationTimestamp: null + labels: + io.kompose.service: scheduler + name: scheduler +spec: + rules: + - http: + paths: + - backend: + serviceName: scheduler + servicePort: 5000 +status: + loadBalancer: {} diff --git a/k8s/kompose/scheduler-service.yaml b/k8s/kompose/scheduler-service.yaml new file mode 100644 index 0000000..19c1ec7 --- /dev/null +++ b/k8s/kompose/scheduler-service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: C:\Go\bin\kompose.exe convert -f docker-compose.build.yaml + kompose.service.expose: "true" + kompose.service.type: loadbalancer + kompose.version: 1.17.0 (a74acad) + creationTimestamp: null + labels: + io.kompose.service: scheduler + name: scheduler +spec: + ports: + - name: "5000" + port: 5000 + targetPort: 5000 + nodePort: 32222 + selector: + io.kompose.service: scheduler + type: LoadBalancer +status: + loadBalancer: {} diff --git a/k8s/kompose/worker-deployment.yaml b/k8s/kompose/worker-deployment.yaml new file mode 100644 index 0000000..1f20e87 --- /dev/null +++ b/k8s/kompose/worker-deployment.yaml @@ -0,0 +1,33 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + annotations: + kompose.cmd: C:\Go\bin\kompose.exe convert -f docker-compose.build.yaml + kompose.version: 1.17.0 (a74acad) + creationTimestamp: null + labels: + io.kompose.service: worker + name: worker +spec: + replicas: 2 + strategy: {} + template: + metadata: + creationTimestamp: null + labels: + io.kompose.service: worker + spec: + containers: + - args: + - ./worker + env: + - name: BITWARDENCLI_APPDATA_DIR + value: /BitwardenCLI + image: vwxyzjn/portwarden-base:1.6.0 + name: worker + resources: {} + stdin: true + tty: true + workingDir: /go/src/github.com/vwxyzjn/portwarden/web/worker + restartPolicy: Always +status: {} diff --git a/web/common.go b/web/common.go new file mode 100644 index 0000000..dcc88d1 --- /dev/null +++ b/web/common.go @@ -0,0 +1,75 @@ +package web + +import ( + "io/ioutil" + "log" + "os" + "path/filepath" + "sync" + + machinery "github.com/RichardKnop/machinery/v1" + "github.com/RichardKnop/machinery/v1/config" + "github.com/go-redis/redis" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + drive "google.golang.org/api/drive/v2" +) + +const ( + BackupDefaultSleepMilliseconds = 300 + PortwardenGoogleDriveBackupFolderName = "portwarden_backup" + MachineryRetryCount = 3 +) + +var ( + GoogleDriveAppConfig *oauth2.Config + RedisClient *redis.Client + MachineryServer *machinery.Server + BITWARDENCLI_APPDATA_DIR string + GlobalMutex sync.Mutex +) + +func InitCommonVars() { + var err error + + // Setup Redis + RedisClient = redis.NewClient(&redis.Options{ + Addr: "redis:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + _, err = RedisClient.Ping().Result() + if err != nil { + log.Fatalf("Unable to parse client secret file to config: %v", err) + } + + // Setup Machinery + var cnf = &config.Config{ + Broker: "redis://redis:6379/", + DefaultQueue: "machinery_tasks", + ResultBackend: "redis://redis:6379/", + AMQP: &config.AMQPConfig{ + Exchange: "machinery_exchange", + ExchangeType: "direct", + BindingKey: "machinery_task", + }, + } + MachineryServer, err = machinery.NewServer(cnf) + if err != nil { + panic(err) + } + + // Setup Google things + absPath, err := filepath.Abs("../portwardenCredentials.json") + if err != nil { + panic(err) + } + credential, err := ioutil.ReadFile(absPath) + if err != nil { + log.Fatalf("Unable to read client secret file: %v", err) + } + GoogleDriveAppConfig, err = google.ConfigFromJSON(credential, "https://www.googleapis.com/auth/userinfo.profile", "email", drive.DriveScope) + + // Get Bitwarden CLI Env Var + BITWARDENCLI_APPDATA_DIR = os.Getenv("BITWARDENCLI_APPDATA_DIR") +} diff --git a/web/credentials.json b/web/credentials.json deleted file mode 100644 index da7263b..0000000 --- a/web/credentials.json +++ /dev/null @@ -1 +0,0 @@ -{"installed":{"client_id":"266239424585-t42ok0rq3f9t3plllutq5vrcg7e5p9i5.apps.googleusercontent.com","project_id":"portwarden","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://www.googleapis.com/oauth2/v3/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"uV2tgocN2ha98uwC5Iv5uNiT","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} \ No newline at end of file diff --git a/web/index.html b/web/index.html deleted file mode 100644 index c0f85a6..0000000 --- a/web/index.html +++ /dev/null @@ -1,48 +0,0 @@ - - - Melody example: chatting - - - - - -
-

Chat

-

-      
-    
- - - - \ No newline at end of file diff --git a/web/main.go b/web/main.go deleted file mode 100644 index 1a8003e..0000000 --- a/web/main.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "io/ioutil" - "log" - - "github.com/vwxyzjn/portwarden/web/server" -) - -func main() { - credential, err := ioutil.ReadFile("portwardenCredentials.json") - if err != nil { - log.Fatalf("Unable to read client secret file: %v", err) - } - ps := server.PortwardenServer{ - Port: 5000, - GoogleDriveAppCredentials: credential, - } - ps.Run() -} diff --git a/web/scheduler/main.go b/web/scheduler/main.go new file mode 100644 index 0000000..c0ae818 --- /dev/null +++ b/web/scheduler/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/vwxyzjn/portwarden/web" + "github.com/vwxyzjn/portwarden/web/scheduler/server" +) + +func main() { + web.InitCommonVars() + ps := server.PortwardenServer{ + Port: 5000, + } + ps.Run() +} diff --git a/web/scheduler/server/controller.go b/web/scheduler/server/controller.go new file mode 100644 index 0000000..ec3f231 --- /dev/null +++ b/web/scheduler/server/controller.go @@ -0,0 +1,136 @@ +package server + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-redis/redis" + "github.com/imdario/mergo" + "github.com/vwxyzjn/portwarden/web" + "golang.org/x/oauth2" +) + +const ( + ErrBindingFromGin = "(debugging message) error binding json" + ErrRetrievingOauthCode = "error retrieving oauth login credentials; try again" + ErrCreatingPortwardenUser = "error creating a portwarden user" + ErrGettingPortwardenUser = "error getting a portwarden user" + ErrLoginWithBitwarden = "error logging in with Bitwarden" + ErrSettingupBackup = "error setting up backup" + ErrBackupNotCancelled = "error cancelling back up" + + MsgSuccessfullyCancelledBackingUp = "successfully cancelled backup process" + + FrontEndBaseAddressTest = "http://localhost:8000/" + FrontEndBaseAddressProd = "" +) + +func EncryptBackupHandler(c *gin.Context) { + var pu PortwardenUser + var opu PortwardenUser + if err := c.ShouldBindJSON(&pu); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ErrBindingFromGin}) + return + } + opu.Email = pu.Email + if err := opu.Get(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ErrGettingPortwardenUser}) + return + } + mergo.Merge(&pu, opu) + if err := pu.LoginWithBitwarden(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ErrLoginWithBitwarden}) + return + } + if err := pu.SetupAutomaticBackup(nil); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": MsgSuccessfullyCancelledBackingUp}) + return + } + if err := pu.Set(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ErrCreatingPortwardenUser}) + return + } +} + +func CancelEncryptBackupHandler(c *gin.Context) { + var pu PortwardenUser + var opu PortwardenUser + if err := c.ShouldBindJSON(&pu); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ErrBindingFromGin}) + return + } + if pu.BackupSetting.WillSetupBackup { + c.JSON(http.StatusBadRequest, gin.H{"error": "", "message": ErrBackupNotCancelled}) + return + } + opu.Email = pu.Email + if err := opu.Get(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ErrGettingPortwardenUser}) + return + } + opu.BackupSetting = pu.BackupSetting + if err := opu.Set(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ErrCreatingPortwardenUser}) + return + } + c.JSON(http.StatusOK, gin.H{"message": MsgSuccessfullyCancelledBackingUp}) +} + +//TODO: GoogleDriveHandler() will return Json with the google login url +// Not sure if it's supposed to call UploadFile() directly +func (ps *PortwardenServer) GetGoogleDriveLoginURLHandler(c *gin.Context) { + c.JSON(200, gin.H{ + "login_url": web.GoogleDriveAppConfig.AuthCodeURL("state-token", oauth2.AccessTypeOffline, oauth2.ApprovalForce), + }) + return +} + +func (ps *PortwardenServer) GetGoogleDriveLoginHandler(c *gin.Context) { + var gdc GoogleDriveCredentials + if err := c.ShouldBind(&gdc); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ErrRetrievingOauthCode}) + return + } + tok, err := web.GoogleDriveAppConfig.Exchange(oauth2.NoContext, gdc.Code) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error(), "message": "Login failure"}) + return + } + gui, err := RetrieveUserEmail(tok) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error(), "message": "Login failure"}) + return + } + pu := &PortwardenUser{Email: gui.Email, GoogleToken: tok} + err = pu.Get() + if err != nil { + if err != redis.Nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ErrGettingPortwardenUser}) + return + } + // Create a user + err = pu.CreateWithGoogle() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ErrCreatingPortwardenUser}) + return + } + c.Redirect(http.StatusMovedPermanently, FrontEndBaseAddressTest+"home/"+"?access_token="+pu.GoogleToken.AccessToken+"&email="+pu.Email+"&will_setup_backup="+strconv.FormatBool(false)) + return + } + // Using info from exisiting user + c.Redirect(http.StatusMovedPermanently, FrontEndBaseAddressTest+"home/"+"?access_token="+tok.AccessToken+"&email="+pu.Email+"&will_setup_backup="+strconv.FormatBool(pu.BackupSetting.WillSetupBackup)) + return +} + +func DecryptBackupHandler(c *gin.Context) { + var dbi DecryptBackupInfo + var err error + if err = c.ShouldBind(&dbi); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ""}) + return + } + if dbi.File, err = c.FormFile("file"); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ""}) + } +} diff --git a/web/server/google_drive.go b/web/scheduler/server/google_drive.go similarity index 54% rename from web/server/google_drive.go rename to web/scheduler/server/google_drive.go index e5481c8..7f75959 100644 --- a/web/server/google_drive.go +++ b/web/scheduler/server/google_drive.go @@ -1,22 +1,22 @@ package server import ( - "crypto/rand" + "bytes" "encoding/json" "fmt" - "io/ioutil" "log" "net/http" "net/url" "os" "os/user" "path/filepath" - "strconv" - "strings" + "time" + + "github.com/vwxyzjn/portwarden/web" - "github.com/antonholmquist/jason" "golang.org/x/net/context" "golang.org/x/oauth2" + drive "google.golang.org/api/drive/v2" ) // GetClient uses a Context and Config to retrieve a Token @@ -91,95 +91,65 @@ func SaveToken(file string, token *oauth2.Token) { json.NewEncoder(f).Encode(token) } -func randStr(strSize int, randType string) string { +// UploadFile upload the fileBytes to Google Drive's portwarden folder +// https://gist.github.com/tzmartin/f5732091783752660b671c20479f519a +func UploadFile(fileBytes []byte, token *oauth2.Token) (*oauth2.Token, error) { + // Get updated access token + tokenSource := web.GoogleDriveAppConfig.TokenSource(oauth2.NoContext, token) + newToken, err := tokenSource.Token() + if err != nil { + return nil, err + } + client := web.GoogleDriveAppConfig.Client(oauth2.NoContext, newToken) + srv, err := drive.New(client) + if err != nil { + return nil, err + } + mimeType := http.DetectContentType(fileBytes) - var dictionary string - - if randType == "alphanum" { - dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + parentId, err := GetOrCreateFolder(srv, web.PortwardenGoogleDriveBackupFolderName) + if err != nil { + return nil, err } - if randType == "alpha" { - dictionary = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + f := &drive.File{Title: time.Now().Format("01-02-2006") + ".portwarden", MimeType: mimeType} + if parentId != "" { + p := &drive.ParentReference{Id: parentId} + f.Parents = []*drive.ParentReference{p} } - if randType == "number" { - dictionary = "0123456789" + _, err = srv.Files.Insert(f).Media(bytes.NewReader(fileBytes)).Do() + if err != nil { + return nil, err } - var bytes = make([]byte, strSize) - rand.Read(bytes) - for k, v := range bytes { - bytes[k] = dictionary[v%byte(len(dictionary))] - } - return string(bytes) + return newToken, nil } -// Multipart upload method -// See https://developers.google.com/drive/v3/web/manage-uploads -// uploadFile() takes in the encrypted byte array to be uploaded, client generated by GetClient() and oauth2 token -func UploadFile(fileBytes []byte, client *http.Client, token *oauth2.Token) error { - - fileMIMEType := http.DetectContentType(fileBytes) - - postURL := "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart" - - // Extract auth or access token from Token file - // See https://godoc.org/gnolang.org/x/oauth2#Token - authToken := token.AccessToken - - boundary := randStr(32, "alphanum") - - uploadData := []byte("\n" + - "--" + boundary + "\n" + - "Content-Type: application/json; charset=" + string('"') + "UTF-8" + string('"') + "\n\n" + - "{ \n" + - string('"') + "name" + string('"') + ":" + string('"') + "test" + string('"') + "\n" + - "} \n\n" + - "--" + boundary + "\n" + - "Content-Type:" + fileMIMEType + "\n\n" + - string(fileBytes) + "\n" + - - "--" + boundary + "--") - - // Post to Drive with RESTful method - request, err := http.NewRequest("POST", postURL, strings.NewReader(string(uploadData))) - if err != nil { - return err +func GetOrCreateFolder(srv *drive.Service, folderName string) (string, error) { + folderId := "" + if folderName == "" { + return "", nil } - request.Header.Add("Host", "www.googleapis.com") - request.Header.Add("Authorization", "Bearer "+authToken) - request.Header.Add("Content-Type", "multipart/related; boundary="+string('"')+boundary+string('"')) - request.Header.Add("Content-Length", strconv.FormatInt(request.ContentLength, 10)) + q := fmt.Sprintf("title=\"%s\" and mimeType=\"application/vnd.google-apps.folder\"", folderName) - // For debugging - //fmt.Println(request) - response, err := client.Do(request) + r, err := srv.Files.List().Q(q).MaxResults(1).Do() if err != nil { - return err + return "", err } - defer response.Body.Close() - body, err := ioutil.ReadAll(response.Body) - if err != nil { - return err + if len(r.Items) > 0 { + folderId = r.Items[0].Id + } else { + // no folder found create new + f := &drive.File{Title: folderName, Description: "Auto Create by gdrive-upload", MimeType: "application/vnd.google-apps.folder"} + r, err := srv.Files.Insert(f).Do() + if err != nil { + return "", err + } + folderId = r.Id } - - // Output the response from Drive API - fmt.Println(string(body)) - - // Extract the uploaded file ID to execute further customization like update the file - jsonAPIreply, err := jason.NewObjectFromBytes(body) - if err != nil { - return err - } - - uploadedFileID, err := jsonAPIreply.GetString("id") - if err != nil { - return err - } - fmt.Println("Uploaded file ID : ", uploadedFileID) - return nil + return folderId, nil } /* diff --git a/web/scheduler/server/middleware.go b/web/scheduler/server/middleware.go new file mode 100644 index 0000000..6972827 --- /dev/null +++ b/web/scheduler/server/middleware.go @@ -0,0 +1,50 @@ +package server + +import ( + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" +) + +const ( + GoogleOauth2TokenContextVariableName = "GoogleOauth2TokenContextVariableName" +) + +func TokenAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + var token string + HeaderAuthorization, ok := c.Request.Header["Authorization"] + if ok && len(HeaderAuthorization) >= 1 { + token = HeaderAuthorization[0] + token = strings.TrimPrefix(token, "Bearer ") + } + + verified, err := VerifyGoogleAccessToekn(token) + if err != nil || !verified { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error(), "message": "Verification status is " + strconv.FormatBool(verified)}) + c.Abort() + return + } + c.Set(GoogleOauth2TokenContextVariableName, token) + c.Next() + } +} + +func CORSMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Max-Age", "86400") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") + c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(200) + } else { + c.Next() + } + } +} diff --git a/web/scheduler/server/model.go b/web/scheduler/server/model.go new file mode 100644 index 0000000..e78e6d1 --- /dev/null +++ b/web/scheduler/server/model.go @@ -0,0 +1,189 @@ +package server + +import ( + "encoding/json" + "errors" + "io/ioutil" + "mime/multipart" + "net/http" + "strconv" + "time" + + "github.com/RichardKnop/machinery/v1/tasks" + "github.com/vwxyzjn/portwarden" + "github.com/vwxyzjn/portwarden/web" + "golang.org/x/oauth2" +) + +const ( + ErrWillNotSetupBackupByUser = "err the user stopped backing up" +) + +type BackupSetting struct { + Passphrase string `json:"passphrase"` + BackupFrequencySeconds int `json:"backup_frequency_seconds"` + WillSetupBackup bool `json:"will_setup_backup"` +} + +type DecryptBackupInfo struct { + File *multipart.FileHeader `form:"file"` + Passphrase string `form:"passphrase"` +} + +type GoogleTokenVerifyResponse struct { + IssuedTo string `json:"issued_to"` + Audience string `json:"audience"` + UserID string `json:"user_id"` + Scope string `json:"scope"` + ExpiresIn int64 `json:"expires_in"` + Email string `json:"email"` + VerifiedEmail bool `json:"verified_email"` + AccessType string `json:"access_type"` +} + +type GoogleDriveCredentials struct { + State string `form:"state"` + Code string `form:"code"` + Scope string `form:"scope"` +} + +type PortwardenUser struct { + Email string `json:"email"` + BitwardenDataJSON []byte `json:"bitwarden_data_json"` + BitwardenSessionKey string `json:"bitwarden_session_key"` + BackupSetting BackupSetting `json:"backup_setting"` + BitwardenLoginCredentials *portwarden.LoginCredentials `json:"bitwarden_login_credentials"` // Not stored in Redis + GoogleUserInfo GoogleUserInfo + GoogleToken *oauth2.Token +} + +type GoogleUserInfo struct { + ID string `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + GivenName string `json:"given_name"` + FamilyName string `json:"family_name"` + Link string `json:"link"` + Picture string `json:"picture"` + Locale string `json:"locale"` +} + +func (pu *PortwardenUser) CreateWithGoogle() error { + var err error + pu.GoogleUserInfo, err = RetrieveUserEmail(pu.GoogleToken) + if err != nil { + return err + } + pu.Email = pu.GoogleUserInfo.Email + err = pu.Set() + if err != nil { + return err + } + return nil +} + +func (pu *PortwardenUser) LoginWithBitwarden() error { + web.GlobalMutex.Lock() + defer web.GlobalMutex.Unlock() + var err error + pu.BitwardenSessionKey, pu.BitwardenDataJSON, err = portwarden.BWLoginGetSessionKeyAndDataJSON(pu.BitwardenLoginCredentials, web.BITWARDENCLI_APPDATA_DIR) + if err != nil { + return err + } + return nil +} + +func (pu *PortwardenUser) SetupAutomaticBackup(eta *time.Time) error { + if !pu.BackupSetting.WillSetupBackup { + return errors.New(ErrWillNotSetupBackupByUser) + } + signature := &tasks.Signature{ + Name: "BackupToGoogleDrive", + Args: []tasks.Arg{ + { + Type: "string", + Value: pu.Email, + }, + }, + ETA: eta, + RetryCount: web.MachineryRetryCount, + } + _, err := web.MachineryServer.SendTask(signature) + if err != nil { + return err + } + return nil +} + +func (pu *PortwardenUser) Set() error { + pu.BitwardenLoginCredentials = &portwarden.LoginCredentials{} + puJson, err := json.Marshal(pu) + if err != nil { + return err + } + err = web.RedisClient.Set(pu.Email, string(puJson), 0).Err() + if err != nil { + panic(err) + } + return nil +} + +func (pu *PortwardenUser) Get() error { + val, err := web.RedisClient.Get(pu.Email).Result() + if err != nil { + return err + } + if err := json.Unmarshal([]byte(val), &pu); err != nil { + return err + } + return nil +} + +func VerifyGoogleAccessToekn(access_token string) (bool, error) { + url := "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=" + access_token + response, err := http.Get(url) + defer response.Body.Close() + if err != nil { + return false, err + } + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return false, err + } + var gtvr GoogleTokenVerifyResponse + if err := json.Unmarshal(body, >vr); err != nil { + return false, err + } + if !gtvr.VerifiedEmail { + return false, errors.New(string(body)) + } + return true, nil +} + +func RetrieveUserEmail(token *oauth2.Token) (GoogleUserInfo, error) { + var gui GoogleUserInfo + postURL := "https://www.googleapis.com/oauth2/v2/userinfo" + request, err := http.NewRequest("GET", postURL, nil) + if err != nil { + return gui, err + } + request.Header.Add("Host", "www.googleapis.com") + request.Header.Add("Authorization", "Bearer "+token.AccessToken) + request.Header.Add("Content-Length", strconv.FormatInt(request.ContentLength, 10)) + + GoogleDriveClient := web.GoogleDriveAppConfig.Client(oauth2.NoContext, token) + response, err := GoogleDriveClient.Do(request) + if err != nil { + return gui, err + } + + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return gui, err + } + if err := json.Unmarshal(body, &gui); err != nil { + return gui, err + } + return gui, nil +} diff --git a/web/server/server.go b/web/scheduler/server/server.go similarity index 56% rename from web/server/server.go rename to web/scheduler/server/server.go index 4d60230..276d4ba 100644 --- a/web/server/server.go +++ b/web/scheduler/server/server.go @@ -1,16 +1,13 @@ package server import ( - "log" - "net/http" "strconv" - "github.com/gin-contrib/cors" + "github.com/davecgh/go-spew/spew" + "github.com/gin-gonic/gin" "golang.org/x/net/context" "golang.org/x/oauth2" - "golang.org/x/oauth2/google" - drive "google.golang.org/api/drive/v2" ) type PortwardenServer struct { @@ -19,29 +16,28 @@ type PortwardenServer struct { GoogleDriveContext context.Context GoogleDriveAppCredentials []byte GoogleDriveAppConfig *oauth2.Config - GoogleClient *http.Client } func (ps *PortwardenServer) Run() { - var err error - ps.GoogleDriveContext = context.Background() - ps.GoogleDriveAppConfig, err = google.ConfigFromJSON(ps.GoogleDriveAppCredentials, drive.DriveScope) - if err != nil { - log.Fatalf("Unable to parse client secret file to config: %v", err) - } - // ps.GoogleClient = GetClient(ps.GoogleDriveContext, ps.GoogleDriveAppConfig) - + spew.Dump("Scheduler Server Started") ps.Router = gin.Default() - ps.Router.Use(cors.Default()) + ps.Router.Use(CORSMiddleware()) ps.Router.GET("/", func(c *gin.Context) { - http.ServeFile(c.Writer, c.Request, "index.html") + c.JSON(200, "Welcome to Portwarden API") }) - ps.Router.POST("/encrypt", EncryptBackupHandler) ps.Router.POST("/decrypt", DecryptBackupHandler) ps.Router.GET("/gdrive/loginUrl", ps.GetGoogleDriveLoginURLHandler) + ps.Router.GET("/gdrive/login", ps.GetGoogleDriveLoginHandler) + ps.Router.Use(TokenAuthMiddleware()) + ps.Router.GET("/test/TokenAuthMiddleware", func(c *gin.Context) { + c.JSON(200, "success") + }) + ps.Router.POST("/encrypt", EncryptBackupHandler) + ps.Router.POST("/encrypt/cancel", CancelEncryptBackupHandler) + ps.Router.Run(":" + strconv.Itoa(ps.Port)) } diff --git a/web/server/token.json b/web/scheduler/server/token.json similarity index 100% rename from web/server/token.json rename to web/scheduler/server/token.json diff --git a/web/server/backup.go b/web/server/backup.go deleted file mode 100644 index 3092b65..0000000 --- a/web/server/backup.go +++ /dev/null @@ -1,28 +0,0 @@ -package server - -import ( - "mime/multipart" - - "github.com/vwxyzjn/portwarden" -) - -const ( - BackupDefaultSleepMilliseconds = 300 -) - -type EncryptBackupInfo struct { - FileNamePrefix string `json:"filename_prefix"` - Passphrase string `json:"passphrase"` - BitwardenLoginCredentials portwarden.LoginCredentials `json:"bitwarden_login_credentials"` -} - -type DecryptBackupInfo struct { - File *multipart.FileHeader `form:"file"` - Passphrase string `form:"passphrase"` -} - -type GoogleDriveCredentials struct { - State string `form:"state"` - Code string `form:"code"` - Scope string `form:"scope"` -} diff --git a/web/server/backup_controller.go b/web/server/backup_controller.go deleted file mode 100644 index 60204fb..0000000 --- a/web/server/backup_controller.go +++ /dev/null @@ -1,73 +0,0 @@ -package server - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/vwxyzjn/portwarden" -) - -const ( - ErrRetrievingOauthCode = "error retrieving oauth login credentials; try again" -) - -func EncryptBackupHandler(c *gin.Context) { - var ebi EncryptBackupInfo - if err := c.ShouldBindJSON(&ebi); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ""}) - return - } - sessionKey, err := portwarden.BWLoginGetSessionKey(&ebi.BitwardenLoginCredentials) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": sessionKey}) - return - } - err = portwarden.CreateBackupFile(ebi.FileNamePrefix, ebi.Passphrase, sessionKey, BackupDefaultSleepMilliseconds) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": sessionKey}) - return - } -} - -//TODO: GoogleDriveHandler() will return Json with the google login url -// Not sure if it's supposed to call UploadFile() directly -func (ps *PortwardenServer) GetGoogleDriveLoginURLHandler(c *gin.Context) { - c.JSON(200, gin.H{ - "login_url": ps.GoogleDriveAppConfig.AuthCodeURL("state-token"), - }) - return -} - -func (ps *PortwardenServer) GetGoogleDriveLoginHandler(c *gin.Context) { - var gdc GoogleDriveCredentials - if err := c.ShouldBind(&gdc); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ErrRetrievingOauthCode}) - return - } - tok, err := ps.GoogleDriveAppConfig.Exchange(ps.GoogleDriveContext, gdc.Code) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ErrRetrievingOauthCode}) - return - } - GoogleDriveClient := ps.GoogleDriveAppConfig.Client(ps.GoogleDriveContext, tok) - fileBytes := []byte("xixix") - err = UploadFile(fileBytes, GoogleDriveClient, tok) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ErrRetrievingOauthCode}) - return - } - c.JSON(200, "Login Successful") - return -} - -func DecryptBackupHandler(c *gin.Context) { - var dbi DecryptBackupInfo - var err error - if err = c.ShouldBind(&dbi); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ""}) - return - } - if dbi.File, err = c.FormFile("file"); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": ""}) - } -} diff --git a/web/worker/main.go b/web/worker/main.go new file mode 100644 index 0000000..0d55713 --- /dev/null +++ b/web/worker/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/vwxyzjn/portwarden" + "github.com/vwxyzjn/portwarden/web" + "github.com/vwxyzjn/portwarden/web/scheduler/server" +) + +func main() { + web.InitCommonVars() + web.MachineryServer.RegisterTasks(map[string]interface{}{ + "BackupToGoogleDrive": BackupToGoogleDrive, + }) + worker := web.MachineryServer.NewWorker("worker_name", 0) + err := worker.Launch() + if err != nil { + panic(err) + } +} + +func BackupToGoogleDrive(email string) error { + web.GlobalMutex.Lock() + defer web.GlobalMutex.Unlock() + name, err := os.Hostname() + if err != nil { + return err + } + fmt.Println("BackupToGoogleDrive called from worker:", name) + pu := server.PortwardenUser{Email: email} + err = pu.Get() + if err != nil { + return err + } + encryptedData, err := portwarden.CreateBackupBytesUsingBitwardenLocalJSON(pu.BitwardenDataJSON, web.BITWARDENCLI_APPDATA_DIR, pu.BackupSetting.Passphrase, pu.BitwardenSessionKey, web.BackupDefaultSleepMilliseconds) + if err != nil { + return err + } + newToken, err := server.UploadFile(encryptedData, pu.GoogleToken) + if err != nil { + return err + } + pu.GoogleToken = newToken + eta := time.Now().UTC().Add(time.Second * time.Duration(pu.BackupSetting.BackupFrequencySeconds)) + err = pu.SetupAutomaticBackup(&eta) + if err != nil { + if err.Error() != server.ErrWillNotSetupBackupByUser { + return err + } else { + fmt.Printf("user %v cancelled backup", pu.Email) + } + } + // Update the access token + err = pu.Set() + if err != nil { + return err + } + return nil +}