├── .bazelrc ├── .circleci ├── config.yml └── image │ ├── Dockerfile │ ├── README.md │ ├── build.sh │ ├── install_bazel.sh │ ├── install_dependencies.sh │ ├── install_redis.sh │ └── versions │ ├── bazel │ ├── redis_4 │ └── redis_5 ├── .gitignore ├── BUILD ├── LICENSE ├── README.md ├── WORKSPACE ├── build_and_load_image_with_docker.sh ├── go.mod ├── go.sum ├── libsodium.BUILD ├── redis_modules_sdk.BUILD ├── src ├── consts.h ├── end.c ├── end.h ├── expire.c ├── expire.h ├── module.c ├── pdel.c ├── pdel.h ├── pget.c ├── pget.h ├── pset.c ├── pset.h ├── start.c ├── start.h ├── utils.c └── utils.h └── tests └── integration ├── BUILD.bazel ├── integration_suite_test.go └── integration_test.go /.bazelrc: -------------------------------------------------------------------------------- 1 | startup \ 2 | --host_jvm_args=-Xmx2g \ 3 | --host_jvm_args=-Xms1024m \ 4 | 5 | build \ 6 | --local_resources=2048,2.0,1.0 \ 7 | --host_force_python=PY2 8 | 9 | test \ 10 | --test_output=errors \ 11 | --test_verbose_timeout_warnings \ 12 | --host_force_python=PY2 13 | 14 | run \ 15 | --host_force_python=PY2 16 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | "redis 4": 4 | working_directory: ~/sessiongate 5 | docker: 6 | - image: thulioassis/sessiongate-ci:0.1.0 7 | steps: 8 | - checkout: 9 | path: ~/sessiongate 10 | - run: 11 | command: cat /var/versions/redis_4 | xargs /opt/bin/install_redis.sh 12 | - run: 13 | command: bazel test --test_output=all //... 14 | "redis 5": 15 | working_directory: ~/sessiongate 16 | docker: 17 | - image: thulioassis/sessiongate-ci:0.1.0 18 | steps: 19 | - checkout: 20 | path: ~/sessiongate 21 | - run: 22 | command: cat /var/versions/redis_5 | xargs /opt/bin/install_redis.sh 23 | - run: 24 | command: bazel test --test_output=all //... 25 | workflows: 26 | version: 2 27 | build_and_test: 28 | jobs: 29 | - redis 4 30 | - redis 5 31 | -------------------------------------------------------------------------------- /.circleci/image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM opensuse/leap 2 | 3 | ADD install_dependencies.sh /opt/bin/install_dependencies.sh 4 | RUN /opt/bin/install_dependencies.sh 5 | 6 | ADD install_bazel.sh /opt/bin/install_bazel.sh 7 | ADD versions/bazel /var/versions/bazel 8 | RUN cat /var/versions/bazel | xargs /opt/bin/install_bazel.sh 9 | 10 | ADD install_redis.sh /opt/bin/install_redis.sh 11 | ADD versions/redis_4 /var/versions/redis_4 12 | ADD versions/redis_5 /var/versions/redis_5 13 | -------------------------------------------------------------------------------- /.circleci/image/README.md: -------------------------------------------------------------------------------- 1 | # Image 2 | 3 | The Docker image for the CircleCI pipeline. 4 | 5 | ## Building 6 | 7 | ```txt 8 | cd $(git rev-parse --show-toplevel)/.circleci/image 9 | ./build.sh 10 | ``` 11 | 12 | ## Testing 13 | 14 | ```txt 15 | cd $(git rev-parse --show-toplevel) 16 | circleci local execute --job "redis 4" 17 | circleci local execute --job "redis 5" 18 | ``` 19 | -------------------------------------------------------------------------------- /.circleci/image/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | image_name="thulioassis/sessiongate-ci:0.1.0" 4 | 5 | docker build -t $image_name . 6 | -------------------------------------------------------------------------------- /.circleci/image/install_bazel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | 5 | version="$1" 6 | 7 | printf "Downloading Bazel ${version}..." 8 | tmp_installer=$(mktemp -t bazel.XXXXXXXX) 9 | curl \ 10 | -o $tmp_installer \ 11 | -L https://github.com/bazelbuild/bazel/releases/download/${version}/bazel-${version}-installer-linux-x86_64.sh \ 12 | 2> /dev/null 13 | printf " ok\n" 14 | 15 | printf "Installing Bazel ${version}..." 16 | sh $tmp_installer > /dev/null 2>&1 17 | printf " ok\n" 18 | -------------------------------------------------------------------------------- /.circleci/image/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | 5 | zypper refresh 6 | zypper dup -y 7 | zypper install -y --type pattern devel_basis 8 | zypper install -y \ 9 | curl \ 10 | docker \ 11 | git \ 12 | java-11-openjdk-devel \ 13 | libstdc++-devel \ 14 | python \ 15 | python-pip \ 16 | python3 \ 17 | unzip \ 18 | which 19 | -------------------------------------------------------------------------------- /.circleci/image/install_redis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | 5 | version="$1" 6 | 7 | printf "Downloading Redis ${version}..." 8 | tmp_redis=$(mktemp --directory -t redis.XXXXXXXX) 9 | curl \ 10 | -L https://github.com/antirez/redis/archive/${version}.tar.gz \ 11 | 2> /dev/null \ 12 | | tar zx -C $tmp_redis 13 | printf " ok\n" 14 | 15 | printf "Compiling Redis ${version}..." 16 | { 17 | cd "${tmp_redis}/redis-${version}" 18 | make -j4 > /dev/null 2>&1 19 | } 20 | printf " ok\n" 21 | 22 | printf "Installing Redis ${version}..." 23 | { 24 | cd "${tmp_redis}/redis-${version}" 25 | make install > /dev/null 2>&1 26 | } 27 | printf " ok\n" 28 | -------------------------------------------------------------------------------- /.circleci/image/versions/bazel: -------------------------------------------------------------------------------- 1 | 0.27.0 2 | -------------------------------------------------------------------------------- /.circleci/image/versions/redis_4: -------------------------------------------------------------------------------- 1 | 4.0.14 2 | -------------------------------------------------------------------------------- /.circleci/image/versions/redis_5: -------------------------------------------------------------------------------- 1 | 5.0.5 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bazel-* 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | load("@bazel_gazelle//:def.bzl", "gazelle") 4 | load( 5 | "@io_bazel_rules_docker//container:container.bzl", 6 | "container_bundle", 7 | "container_image", 8 | "container_push", 9 | ) 10 | 11 | gazelle( 12 | name = "gazelle", 13 | prefix = "github.com/f0rmiga/sessiongate", 14 | ) 15 | 16 | cc_library( 17 | name = "sessiongate", 18 | srcs = [ 19 | "src/end.c", 20 | "src/expire.c", 21 | "src/module.c", 22 | "src/pdel.c", 23 | "src/pget.c", 24 | "src/pset.c", 25 | "src/start.c", 26 | "src/utils.c", 27 | ], 28 | hdrs = [ 29 | "src/consts.h", 30 | "src/end.h", 31 | "src/expire.h", 32 | "src/pdel.h", 33 | "src/pget.h", 34 | "src/pset.h", 35 | "src/start.h", 36 | "src/utils.h", 37 | ], 38 | copts = ["-std=c11"], 39 | deps = [ 40 | "@libsodium//:sodium", 41 | "@redis_modules_sdk//:redis_modules_sdk", 42 | ], 43 | alwayslink = 1, 44 | ) 45 | 46 | cc_binary( 47 | name = "sessiongate.so", 48 | linkshared = True, 49 | deps = [ 50 | ":sessiongate", 51 | ], 52 | ) 53 | 54 | container_image( 55 | name = "_sessiongate_image", 56 | base = "@redis//image", 57 | files = [":sessiongate.so"], 58 | cmd = [ 59 | "redis-server", 60 | "--loadmodule", 61 | "/sessiongate.so", 62 | ], 63 | ) 64 | 65 | container_bundle( 66 | name = "sessiongate_image", 67 | images = { 68 | "thulioassis/sessiongate:latest": ":_sessiongate_image", 69 | }, 70 | ) 71 | 72 | container_push( 73 | name = "push_sessiongate_image", 74 | image = ":_sessiongate_image", 75 | format = "Docker", 76 | registry = "index.docker.io", 77 | repository = "thulioassis/sessiongate", 78 | tag = "latest", 79 | ) 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Thulio Ferraz Assis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/f0rmiga/sessiongate/tree/master.svg?style=svg)](https://circleci.com/gh/f0rmiga/sessiongate/tree/master) 2 | 3 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ff0rmiga%2Fsessiongate.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Ff0rmiga%2Fsessiongate?ref=badge_large) 4 | 5 | # Session Gate 6 | 7 | Session Gate is a Redis module to ease session management using tokens. This module does NOT do user 8 | management, don't get confused. In the real world, most of the sessions are related to a user but do 9 | your crazy logic to manage the users the way you want. 10 | 11 | ## How it works 12 | 13 | This module provides creation and administration of sessions using tokens. Each session can have 14 | more than one payload and those payloads can be accessed individually. A single instance of Redis 15 | loaded with this module can handle sessions from multiple applications. 16 | 17 | Redis is a high performance, in-memory data structure store. This module is built on top of it, 18 | meaning this module operates in a very similar way Redis itself operates. 19 | 20 | To access this module, any Redis compatible driver can be used. The commands to operate this module 21 | are exposed like any other Redis command. 22 | 23 | To know more about Redis modules, follow this [link](http://antirez.com/news/106). 24 | 25 | ## How to build 26 | 27 | The module is written in C and uses Bazel to build. Bazel manages all the dependencies. :D 28 | 29 | The dependencies are: 30 | 31 | - [Redis Modules SDK](https://github.com/RedisLabs/RedisModulesSDK) 32 | - [Sodium crypto library (libsodium)](https://download.libsodium.org/doc/) 33 | 34 | ### Building on Linux/OS X 35 | 36 | Run: 37 | 38 | ```txt 39 | bazel build //:sessiongate.so 40 | ``` 41 | 42 | ### Building on Windows 43 | 44 | ¯\\\_(ツ)\_/¯ 45 | 46 | ### Building the Docker image 47 | 48 | On Linux, Docker is necessary only if you want to load the built image into the daemon (which is 49 | almost always the case). On OS X and Windows, Docker is always required to be installed. 50 | 51 | On Linux, simply run `bazel run //:sessiongate_image`. It will build and load the image. 52 | 53 | On OS X and Windows, run the script `./build_and_load_image_with_docker.sh`, which uses Docker in 54 | Docker for building the module and appending it as a layer on top of the Redis image. 55 | 56 | ## How to run tests 57 | 58 | Tests are located under `tests/` directory and are written in Python. You should have Python 2.7+ 59 | installed in order to run the tests. Run: 60 | 61 | ```txt 62 | bazel test //... 63 | ``` 64 | 65 | For verbose tests run: 66 | 67 | ```txt 68 | bazel test --test_output=all //... 69 | ``` 70 | 71 | ## Loading the module for use 72 | 73 | The module can be loaded in Redis 4+. The most convenient way to do that is by passing --loadmodule 74 | parameter when starting the Redis server: 75 | 76 | ```txt 77 | redis-server --loadmodule 78 | ``` 79 | 80 | For example, starting Redis open to the world and the Session Gate loaded: 81 | 82 | ```txt 83 | redis-server --protected-mode no --loadmodule $(pwd)/bazel-bin/sessiongate.so 84 | ``` 85 | 86 | Just make sure to pass the right `sessiongate.so` path value to the --loadmodule parameter. 87 | 88 | ## Commands 89 | 90 | ### Start a session 91 | 92 | Command: `SESSIONGATE.START ` 93 | 94 | - `` is the secret string used by the HMAC algorithm to generate the token signature. 95 | - `` is the positive integer that represents the seconds that the session will live. If set to 96 | 0, the session expires immediately. 97 | 98 | #### Example 99 | 100 | ```txt 101 | SESSIONGATE.START 'qwerty' 300 102 | ``` 103 | 104 | Returns: a token that is used to manage the session. 105 | 106 | ### Set a session TTL 107 | 108 | Command: `SESSIONGATE.EXPIRE ` 109 | 110 | - `` is the secret string used by the HMAC algorithm to verify the token signature. 111 | - `` is the token returned by the START command. 112 | - `` is the positive integer that represents the seconds that the session will live. If set to 113 | 0, the session expires immediately. 114 | 115 | #### Example 116 | 117 | ```txt 118 | SESSIONGATE.EXPIRE 'qwerty' 300 119 | ``` 120 | 121 | Returns: OK. 122 | 123 | ### Set a session payload 124 | 125 | Command: `SESSIONGATE.PSET ` 126 | 127 | - `` is the secret string used by the HMAC algorithm to verify the token signature. 128 | - `` is the token returned by the START command. 129 | - `` is the payload name that is used to identify the payload data. 130 | - `` is the payload data. It can be any string, for example, a JSON stringified 131 | object. 132 | 133 | #### Example 134 | 135 | ```txt 136 | SESSIONGATE.PSET 'qwerty' 'user' '{"name":"John Doe"}' 137 | ``` 138 | 139 | Returns: OK. 140 | 141 | ### Get a session payload 142 | 143 | Command: `SESSIONGATE.PGET ` 144 | 145 | - `` is the secret string used by the HMAC algorithm to verify the token signature. 146 | - `` is the token returned by the START command. 147 | - `` is the payload name that is used to retrieve the payload data. 148 | 149 | #### Example 150 | 151 | ```txt 152 | SESSIONGATE.PGET 'qwerty' 'user' 153 | ``` 154 | 155 | Returns: a string containing the payload data. 156 | 157 | ### Delete a session payload 158 | 159 | Command: `SESSIONGATE.PDEL ` 160 | 161 | - `` is the secret string used by the HMAC algorithm to verify the token signature. 162 | - `` is the token returned by the START command. 163 | - `` is the payload name that is used to identify the payload data being deleted. 164 | 165 | #### Example 166 | 167 | ```txt 168 | SESSIONGATE.PDEL 'qwerty' 'user' 169 | ``` 170 | 171 | Returns: OK. 172 | 173 | ### End a session 174 | 175 | Command: `SESSIONGATE.END ` 176 | 177 | - `` is the secret string used by the HMAC algorithm to verify the token signature. 178 | - `` is the token returned by the START command. 179 | 180 | #### Example 181 | 182 | ```txt 183 | SESSIONGATE.END 'qwerty' 184 | ``` 185 | 186 | Returns: OK. 187 | 188 | ## Specific language drivers 189 | 190 | Here is a list of drivers implemented in specific languages to ease the use of the SessionGate 191 | module: 192 | 193 | ### Go 194 | 195 | https://github.com/f0rmiga/sessiongate-go 196 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "sessiongate") 2 | 3 | load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository", "new_git_repository") 4 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 5 | 6 | http_archive( 7 | name = "bazel_skylib", 8 | sha256 = "2ef429f5d7ce7111263289644d233707dba35e39696377ebab8b0bc701f7818e", 9 | urls = ["https://github.com/bazelbuild/bazel-skylib/releases/download/0.8.0/bazel-skylib.0.8.0.tar.gz"], 10 | ) 11 | 12 | # C dependencies. 13 | new_git_repository( 14 | name = "redis_modules_sdk", 15 | build_file = "//:redis_modules_sdk.BUILD", 16 | commit = "3e4daab6dcbc881d8c17406235d156de55be4fc7", 17 | remote = "https://github.com/RedisLabs/RedisModulesSDK.git", 18 | shallow_since = "1503497108 +0300", 19 | ) 20 | 21 | libsodium_version = "1.0.18" 22 | 23 | http_archive( 24 | name = "libsodium", 25 | build_file = "//:libsodium.BUILD", 26 | sha256 = "6f504490b342a4f8a4c4a02fc9b866cbef8622d5df4e5452b46be121e46636c1", 27 | strip_prefix = "libsodium-{}".format(libsodium_version), 28 | urls = [ 29 | "https://download.libsodium.org/libsodium/releases/libsodium-{}.tar.gz".format(libsodium_version), 30 | ], 31 | ) 32 | 33 | # Docker rules. 34 | http_archive( 35 | name = "io_bazel_rules_docker", 36 | sha256 = "87fc6a2b128147a0a3039a2fd0b53cc1f2ed5adb8716f50756544a572999ae9a", 37 | strip_prefix = "rules_docker-0.8.1", 38 | urls = ["https://github.com/bazelbuild/rules_docker/archive/v0.8.1.tar.gz"], 39 | ) 40 | 41 | load( 42 | "@io_bazel_rules_docker//repositories:repositories.bzl", 43 | container_repositories = "repositories", 44 | ) 45 | 46 | container_repositories() 47 | 48 | load( 49 | "@io_bazel_rules_docker//container:container.bzl", 50 | "container_pull", 51 | ) 52 | 53 | container_pull( 54 | name = "redis", 55 | registry = "index.docker.io", 56 | repository = "library/redis", 57 | tag = "5.0.5-stretch", 58 | digest = "sha256:adcf62f378efe1187d2f72c6f0ecdf86ab2173a9e1c3c9f4fe4bb89060f5362f", 59 | ) 60 | 61 | # Go rules. 62 | http_archive( 63 | name = "io_bazel_rules_go", 64 | urls = [ 65 | "https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/0.18.6/rules_go-0.18.6.tar.gz", 66 | "https://github.com/bazelbuild/rules_go/releases/download/0.18.6/rules_go-0.18.6.tar.gz", 67 | ], 68 | sha256 = "f04d2373bcaf8aa09bccb08a98a57e721306c8f6043a2a0ee610fd6853dcde3d", 69 | ) 70 | 71 | http_archive( 72 | name = "bazel_gazelle", 73 | urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.17.0/bazel-gazelle-0.17.0.tar.gz"], 74 | sha256 = "3c681998538231a2d24d0c07ed5a7658cb72bfb5fd4bf9911157c0e9ac6a2687", 75 | ) 76 | 77 | load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies", "go_register_toolchains") 78 | 79 | go_rules_dependencies() 80 | 81 | go_register_toolchains() 82 | 83 | load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository") 84 | 85 | gazelle_dependencies() 86 | 87 | go_repository( 88 | name = "com_github_fsnotify_fsnotify", 89 | importpath = "github.com/fsnotify/fsnotify", 90 | tag = "v1.4.7", 91 | ) 92 | 93 | go_repository( 94 | name = "com_github_golang_protobuf", 95 | importpath = "github.com/golang/protobuf", 96 | tag = "v1.2.0", 97 | ) 98 | 99 | go_repository( 100 | name = "com_github_hpcloud_tail", 101 | importpath = "github.com/hpcloud/tail", 102 | tag = "v1.0.0", 103 | ) 104 | 105 | go_repository( 106 | name = "com_github_onsi_ginkgo", 107 | importpath = "github.com/onsi/ginkgo", 108 | tag = "v1.8.0", 109 | ) 110 | 111 | go_repository( 112 | name = "com_github_onsi_gomega", 113 | importpath = "github.com/onsi/gomega", 114 | tag = "v1.5.0", 115 | ) 116 | 117 | go_repository( 118 | name = "in_gopkg_check_v1", 119 | commit = "20d25e280405", 120 | importpath = "gopkg.in/check.v1", 121 | ) 122 | 123 | go_repository( 124 | name = "in_gopkg_fsnotify_v1", 125 | importpath = "gopkg.in/fsnotify.v1", 126 | tag = "v1.4.7", 127 | ) 128 | 129 | go_repository( 130 | name = "in_gopkg_tomb_v1", 131 | commit = "dd632973f1e7", 132 | importpath = "gopkg.in/tomb.v1", 133 | ) 134 | 135 | go_repository( 136 | name = "in_gopkg_yaml_v2", 137 | importpath = "gopkg.in/yaml.v2", 138 | tag = "v2.2.1", 139 | ) 140 | 141 | go_repository( 142 | name = "org_golang_x_net", 143 | commit = "161cd47e91fd", 144 | importpath = "golang.org/x/net", 145 | ) 146 | 147 | go_repository( 148 | name = "org_golang_x_sync", 149 | commit = "1d60e4601c6f", 150 | importpath = "golang.org/x/sync", 151 | ) 152 | 153 | go_repository( 154 | name = "org_golang_x_sys", 155 | commit = "04f50cda93cb", 156 | importpath = "golang.org/x/sys", 157 | ) 158 | 159 | go_repository( 160 | name = "org_golang_x_text", 161 | importpath = "golang.org/x/text", 162 | tag = "v0.3.0", 163 | ) 164 | -------------------------------------------------------------------------------- /build_and_load_image_with_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | project_mount="/home/sessiongate" 4 | 5 | # Enables Docker in Docker and mounts the project inside the container. 6 | docker run -it \ 7 | -v /var/run/docker.sock:/var/run/docker.sock \ 8 | -v $(git rev-parse --show-toplevel):"${project_mount}" \ 9 | thulioassis/sessiongate-ci \ 10 | /bin/sh -c "cd ${project_mount}; bazel run //:sessiongate_image" 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/f0rmiga/sessiongate 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/onsi/ginkgo v1.8.0 7 | github.com/onsi/gomega v1.5.0 8 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 2 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 3 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 4 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 5 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 6 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 7 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 8 | github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= 9 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 10 | github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= 11 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 12 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= 13 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 14 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 15 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 16 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 17 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= 18 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 20 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 24 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 25 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 26 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 27 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 28 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 29 | -------------------------------------------------------------------------------- /libsodium.BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | genrule( 4 | name = "configure_sh", 5 | outs = [ 6 | "configure.sh", 7 | ], 8 | cmd = """ 9 | cat > $@ <<"EOF" 10 | #! /bin/sh 11 | ./external/libsodium/configure >> /dev/null 12 | EOF""" 13 | ) 14 | 15 | filegroup( 16 | name = "all", 17 | srcs = glob(["**/*"]) 18 | ) 19 | 20 | VERSION_H_PATH = "src/libsodium/include/sodium/version.h" 21 | genrule( 22 | name = "version_h", 23 | srcs = [ 24 | ":all", 25 | ], 26 | outs = [ 27 | VERSION_H_PATH, 28 | ], 29 | cmd = "$(location :configure_sh) && cat {} > $(@)".format(VERSION_H_PATH), 30 | tools = [":configure_sh"], 31 | ) 32 | 33 | filegroup( 34 | name = "headers", 35 | srcs = glob( 36 | ["**/*.h"], 37 | exclude = ["test/**/*"], 38 | ), 39 | ) 40 | 41 | filegroup( 42 | name = "srcs", 43 | srcs = glob( 44 | ["**/*.c"], 45 | exclude = ["test/**/*"], 46 | ), 47 | ) 48 | 49 | cc_library( 50 | name = "sodium", 51 | hdrs = [ 52 | ":headers", 53 | ":version_h", 54 | ], 55 | srcs = [ 56 | ":srcs", 57 | ], 58 | includes = [ 59 | "src/libsodium/include", 60 | "src/libsodium/include/sodium", 61 | ], 62 | copts = [ 63 | "-Wno-cpp", 64 | "-Wno-unknown-pragmas", 65 | "-Wno-unknown-warning-option", 66 | "-Wno-unused-function", 67 | "-Wno-unused-value", 68 | "-Wno-unused-variable", 69 | "-Wno-#warnings", 70 | ], 71 | ) 72 | 73 | cc_binary( 74 | name = "libsodium", 75 | deps = [":sodium"], 76 | linkstatic = 1, 77 | ) 78 | -------------------------------------------------------------------------------- /redis_modules_sdk.BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | filegroup( 4 | name = "headers", 5 | srcs = [ 6 | "redismodule.h", 7 | "rmutil/util.h", 8 | ], 9 | ) 10 | 11 | cc_library( 12 | name = "redis_modules_sdk", 13 | hdrs = [ 14 | ":headers", 15 | ], 16 | includes = [ 17 | ".", 18 | ], 19 | ) 20 | -------------------------------------------------------------------------------- /src/consts.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define SESSION_ID_DICTIONARY "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 4 | #define SESSION_ID_DICTIONARY_STRLEN strlen(SESSION_ID_DICTIONARY) 5 | #define SESSION_ID_STRLEN (size_t)16 // SESSION_ID_DICTIONARY_STRLEN ^ 16 possible combinations. 6 | 7 | // The signature is encoded as hex string. 8 | // Each byte uses 2 characters. 256 bits (SHA-256) = 32 bytes. 32 bytes * 2 = 64 bytes. 9 | #define SIGNATURE_STRLEN (size_t)64 10 | 11 | #define SESSION_ENCODING_VERSION (uint8_t)1 12 | 13 | // The token version is used for future token compatibilities. 14 | #define TOKEN_VERSION "v1" 15 | #define TOKEN_VERSION_STRLEN strlen(TOKEN_VERSION) 16 | // TOKEN_VERSION_STRLEN + 1 dot + SESSION_ID_STRLEN + 1 dot + SIGNATURE_STRLEN. 17 | #define TOKEN_STRLEN (size_t)(TOKEN_VERSION_STRLEN + 1 + SESSION_ID_STRLEN + 1 + SIGNATURE_STRLEN) 18 | 19 | #define PAYLOAD_NAME_MAX_STRLEN (size_t)200 20 | #define PAYLOAD_DATA_MAX_STRLEN (size_t)1e6 * 8 // 8 MB. 21 | -------------------------------------------------------------------------------- /src/end.c: -------------------------------------------------------------------------------- 1 | #include "end.h" 2 | 3 | int EndCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 4 | if (argc != 3) 5 | return RedisModule_WrongArity(ctx); 6 | RedisModule_AutoMemory(ctx); 7 | 8 | // Extract and validate it. 9 | size_t signKeyLen; 10 | const char *signKey = RedisModule_StringPtrLen(argv[1], &signKeyLen); 11 | if (signKeyLen == 0) 12 | return RedisModule_ReplyWithError( 13 | ctx, " must have at least one character"); 14 | 15 | // Extract and validate it. 16 | size_t tokenLen; 17 | const char *token = RedisModule_StringPtrLen(argv[2], &tokenLen); 18 | if (tokenLen == 0) 19 | return RedisModule_ReplyWithError( 20 | ctx, " must have at least one character"); 21 | else if (tokenLen != TOKEN_STRLEN) 22 | return RedisModule_ReplyWithError(ctx, " format is invalid"); 23 | 24 | // Parse the token. 25 | char tokenVersion[TOKEN_VERSION_STRLEN + 1]; 26 | char sessionId[SESSION_ID_STRLEN + 1]; 27 | char signature[SIGNATURE_STRLEN + 1]; 28 | parseToken(token, tokenVersion, sessionId, signature); 29 | 30 | // Recreate the signature of the session id and compare with the signature 31 | // contained in the token. 32 | char signatureCheck[SIGNATURE_STRLEN + 1]; 33 | signData((const unsigned char *)signKey, signKeyLen, 34 | (const unsigned char *)sessionId, SESSION_ID_STRLEN, signatureCheck); 35 | if (strncmp(signature, signatureCheck, SIGNATURE_STRLEN) != 0) 36 | return RedisModule_ReplyWithError( 37 | ctx, "the signature contained in is invalid"); 38 | 39 | // Check if the signature is the same stored in the session. 40 | RedisModuleString *sessionSignatureKeyStr = 41 | RedisModule_CreateStringPrintf(ctx, "sg-session:%s:signature", sessionId); 42 | RedisModuleKey *redisKey = RedisModule_OpenKey( 43 | ctx, sessionSignatureKeyStr, REDISMODULE_READ | REDISMODULE_WRITE); 44 | if (RedisModule_KeyType(redisKey) != REDISMODULE_KEYTYPE_STRING) { 45 | RedisModule_CloseKey(redisKey); 46 | return RedisModule_ReplyWithError( 47 | ctx, "the session id contained in does not exist"); 48 | } 49 | size_t signatureStoredLen; 50 | char *signatureStored = 51 | RedisModule_StringDMA(redisKey, &signatureStoredLen, REDISMODULE_READ); 52 | if (strncmp(signature, signatureStored, SIGNATURE_STRLEN) != 0) { 53 | RedisModule_CloseKey(redisKey); 54 | return RedisModule_ReplyWithError( 55 | ctx, "the signature contained in seems to be valid, but is " 56 | "different from the stored signature in the session"); 57 | } 58 | 59 | // Delete the signature. 60 | RedisModule_DeleteKey(redisKey); 61 | RedisModule_CloseKey(redisKey); 62 | 63 | // Delete the payloads. 64 | RedisModuleString *sessionPayloadsKeyStr = 65 | RedisModule_CreateStringPrintf(ctx, "sg-session:%s:payloads", sessionId); 66 | redisKey = RedisModule_OpenKey(ctx, sessionPayloadsKeyStr, REDISMODULE_WRITE); 67 | if (RedisModule_KeyType(redisKey) != REDISMODULE_KEYTYPE_EMPTY) 68 | RedisModule_DeleteKey(redisKey); 69 | RedisModule_CloseKey(redisKey); 70 | 71 | RedisModule_ReplyWithSimpleString(ctx, "OK"); 72 | return REDISMODULE_OK; 73 | } 74 | -------------------------------------------------------------------------------- /src/end.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "redismodule.h" 6 | 7 | #include "consts.h" 8 | #include "utils.h" 9 | 10 | /* 11 | * sessiongate.end 12 | * Ends a session. 13 | * Returns OK. 14 | */ 15 | int EndCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc); 16 | -------------------------------------------------------------------------------- /src/expire.c: -------------------------------------------------------------------------------- 1 | #include "expire.h" 2 | 3 | int ExpireCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 4 | if (argc != 4) 5 | return RedisModule_WrongArity(ctx); 6 | RedisModule_AutoMemory(ctx); 7 | 8 | // Extract and validate it. 9 | size_t signKeyLen; 10 | const char *signKey = RedisModule_StringPtrLen(argv[1], &signKeyLen); 11 | if (signKeyLen == 0) 12 | return RedisModule_ReplyWithError( 13 | ctx, " must have at least one character"); 14 | 15 | // Extract and validate it. 16 | size_t tokenLen; 17 | const char *token = RedisModule_StringPtrLen(argv[2], &tokenLen); 18 | if (tokenLen == 0) 19 | return RedisModule_ReplyWithError( 20 | ctx, " must have at least one character"); 21 | else if (tokenLen != TOKEN_STRLEN) 22 | return RedisModule_ReplyWithError(ctx, " format is invalid"); 23 | 24 | // Extract and validate it. 25 | long long ttl; 26 | if (RedisModule_StringToLongLong(argv[3], &ttl) != REDISMODULE_OK) 27 | return RedisModule_ReplyWithError( 28 | ctx, " must be a valid integer that represents seconds"); 29 | if (ttl < 0) 30 | return RedisModule_ReplyWithError( 31 | ctx, " must be a valid integer that represents seconds"); 32 | 33 | // Parse the token. 34 | char tokenVersion[TOKEN_VERSION_STRLEN + 1]; 35 | char sessionId[SESSION_ID_STRLEN + 1]; 36 | char signature[SIGNATURE_STRLEN + 1]; 37 | parseToken(token, tokenVersion, sessionId, signature); 38 | 39 | // Recreate the signature of the session id and compare with the signature 40 | // contained in the token. 41 | char signatureCheck[SIGNATURE_STRLEN + 1]; 42 | signData((const unsigned char *)signKey, signKeyLen, 43 | (const unsigned char *)sessionId, SESSION_ID_STRLEN, signatureCheck); 44 | if (strncmp(signature, signatureCheck, SIGNATURE_STRLEN) != 0) 45 | return RedisModule_ReplyWithError( 46 | ctx, "the signature contained in is invalid"); 47 | 48 | // Check if the signature is the same stored in the session. 49 | RedisModuleString *sessionSignatureKeyStr = 50 | RedisModule_CreateStringPrintf(ctx, "sg-session:%s:signature", sessionId); 51 | RedisModuleKey *redisKey = RedisModule_OpenKey( 52 | ctx, sessionSignatureKeyStr, REDISMODULE_READ | REDISMODULE_WRITE); 53 | if (RedisModule_KeyType(redisKey) != REDISMODULE_KEYTYPE_STRING) { 54 | RedisModule_CloseKey(redisKey); 55 | return RedisModule_ReplyWithError( 56 | ctx, "the session id contained in does not exist"); 57 | } 58 | size_t signatureStoredLen; 59 | char *signatureStored = 60 | RedisModule_StringDMA(redisKey, &signatureStoredLen, REDISMODULE_READ); 61 | if (strncmp(signature, signatureStored, SIGNATURE_STRLEN) != 0) { 62 | RedisModule_CloseKey(redisKey); 63 | return RedisModule_ReplyWithError( 64 | ctx, "the signature contained in seems to be valid, but is " 65 | "different from the stored signature in the session"); 66 | } 67 | 68 | // Set the TTL for the signature key. 69 | RedisModule_SetExpire(redisKey, ttl * 1000); 70 | RedisModule_CloseKey(redisKey); 71 | 72 | // Set the TTL for the payloads key if it exists. 73 | RedisModuleString *sessionPayloadsKeyStr = 74 | RedisModule_CreateStringPrintf(ctx, "sg-session:%s:payloads", sessionId); 75 | redisKey = RedisModule_OpenKey(ctx, sessionPayloadsKeyStr, REDISMODULE_WRITE); 76 | if (RedisModule_KeyType(redisKey) == REDISMODULE_KEYTYPE_HASH) 77 | RedisModule_SetExpire(redisKey, ttl * 1000); 78 | 79 | RedisModule_CloseKey(redisKey); 80 | 81 | RedisModule_ReplyWithSimpleString(ctx, "OK"); 82 | return REDISMODULE_OK; 83 | } 84 | -------------------------------------------------------------------------------- /src/expire.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "redismodule.h" 6 | 7 | #include "consts.h" 8 | #include "utils.h" 9 | 10 | /* 11 | * sessiongate.expire 12 | * Sets the TTL of a session. 13 | * Returns OK. 14 | */ 15 | int ExpireCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc); 16 | -------------------------------------------------------------------------------- /src/module.c: -------------------------------------------------------------------------------- 1 | #include "redismodule.h" 2 | #include "rmutil/util.h" 3 | 4 | #include "end.h" 5 | #include "expire.h" 6 | #include "pdel.h" 7 | #include "pget.h" 8 | #include "pset.h" 9 | #include "start.h" 10 | #include "utils.h" 11 | 12 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) 13 | __attribute__((visibility("default"))); 14 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, 15 | int argc) { 16 | REDISMODULE_NOT_USED(argv); 17 | REDISMODULE_NOT_USED(argc); 18 | 19 | // Register the module. 20 | if (RedisModule_Init(ctx, "sessiongate", 1, REDISMODULE_APIVER_1) == 21 | REDISMODULE_ERR) 22 | return REDISMODULE_ERR; 23 | 24 | // Register functions. 25 | RMUtil_RegisterWriteCmd(ctx, "sessiongate.start", StartCommand); 26 | RMUtil_RegisterWriteCmd(ctx, "sessiongate.end", EndCommand); 27 | RMUtil_RegisterWriteCmd(ctx, "sessiongate.pset", PSetCommand); 28 | RMUtil_RegisterWriteCmd(ctx, "sessiongate.pget", PGetCommand); 29 | RMUtil_RegisterWriteCmd(ctx, "sessiongate.pdel", PDelCommand); 30 | RMUtil_RegisterWriteCmd(ctx, "sessiongate.expire", ExpireCommand); 31 | 32 | return REDISMODULE_OK; 33 | } 34 | -------------------------------------------------------------------------------- /src/pdel.c: -------------------------------------------------------------------------------- 1 | #include "pset.h" 2 | 3 | int PDelCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 4 | if (argc != 4) 5 | return RedisModule_WrongArity(ctx); 6 | RedisModule_AutoMemory(ctx); 7 | 8 | // Extract and validate it. 9 | size_t signKeyLen; 10 | const char *signKey = RedisModule_StringPtrLen(argv[1], &signKeyLen); 11 | if (signKeyLen == 0) 12 | return RedisModule_ReplyWithError( 13 | ctx, " must have at least one character"); 14 | 15 | // Extract and validate it. 16 | size_t tokenLen; 17 | const char *token = RedisModule_StringPtrLen(argv[2], &tokenLen); 18 | if (tokenLen == 0) 19 | return RedisModule_ReplyWithError( 20 | ctx, " must have at least one character"); 21 | else if (tokenLen != TOKEN_STRLEN) 22 | return RedisModule_ReplyWithError(ctx, " format is invalid"); 23 | 24 | // Parse the token. 25 | char tokenVersion[TOKEN_VERSION_STRLEN + 1]; 26 | char sessionId[SESSION_ID_STRLEN + 1]; 27 | char signature[SIGNATURE_STRLEN + 1]; 28 | parseToken(token, tokenVersion, sessionId, signature); 29 | 30 | // Recreate the signature of the session id and compare with the signature 31 | // contained in the token. 32 | char signatureCheck[SIGNATURE_STRLEN + 1]; 33 | signData((const unsigned char *)signKey, signKeyLen, 34 | (const unsigned char *)sessionId, SESSION_ID_STRLEN, signatureCheck); 35 | if (strncmp(signature, signatureCheck, SIGNATURE_STRLEN) != 0) 36 | return RedisModule_ReplyWithError( 37 | ctx, "the signature contained in is invalid"); 38 | 39 | // Check if the signature is the same stored in the session. 40 | RedisModuleString *sessionSignatureKeyStr = 41 | RedisModule_CreateStringPrintf(ctx, "sg-session:%s:signature", sessionId); 42 | RedisModuleKey *redisKey = RedisModule_OpenKey( 43 | ctx, sessionSignatureKeyStr, REDISMODULE_READ | REDISMODULE_WRITE); 44 | if (RedisModule_KeyType(redisKey) != REDISMODULE_KEYTYPE_STRING) { 45 | RedisModule_CloseKey(redisKey); 46 | return RedisModule_ReplyWithError( 47 | ctx, "the session id contained in does not exist"); 48 | } 49 | size_t signatureStoredLen; 50 | char *signatureStored = 51 | RedisModule_StringDMA(redisKey, &signatureStoredLen, REDISMODULE_READ); 52 | if (strncmp(signature, signatureStored, SIGNATURE_STRLEN) != 0) { 53 | RedisModule_CloseKey(redisKey); 54 | return RedisModule_ReplyWithError( 55 | ctx, "the signature contained in seems to be valid, but is " 56 | "different from the stored signature in the session"); 57 | } 58 | 59 | RedisModule_CloseKey(redisKey); 60 | 61 | // Extract and validate it. 62 | const RedisModuleString *payloadName = argv[3]; 63 | size_t payloadNameLen; 64 | RedisModule_StringPtrLen(payloadName, &payloadNameLen); 65 | if (payloadNameLen == 0) 66 | return RedisModule_ReplyWithError( 67 | ctx, " must have at least one character"); 68 | else if (payloadNameLen > PAYLOAD_NAME_MAX_STRLEN) { 69 | char msg[128]; 70 | sprintf(msg, 71 | " length exceeds the maximum value allowed of %zu", 72 | PAYLOAD_NAME_MAX_STRLEN); 73 | return RedisModule_ReplyWithError(ctx, msg); 74 | } 75 | 76 | // Delete the payload. 77 | RedisModuleString *sessionPayloadsKeyStr = 78 | RedisModule_CreateStringPrintf(ctx, "sg-session:%s:payloads", sessionId); 79 | redisKey = RedisModule_OpenKey(ctx, sessionPayloadsKeyStr, REDISMODULE_WRITE); 80 | if (RedisModule_KeyType(redisKey) != REDISMODULE_KEYTYPE_HASH) { 81 | RedisModule_CloseKey(redisKey); 82 | return RedisModule_ReplyWithError( 83 | ctx, "the requested does not exist"); 84 | } 85 | RedisModule_HashSet(redisKey, REDISMODULE_HASH_NONE, payloadName, 86 | REDISMODULE_HASH_DELETE, NULL); 87 | RedisModule_CloseKey(redisKey); 88 | 89 | RedisModule_ReplyWithSimpleString(ctx, "OK"); 90 | return REDISMODULE_OK; 91 | } 92 | -------------------------------------------------------------------------------- /src/pdel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "redismodule.h" 6 | 7 | #include "consts.h" 8 | #include "utils.h" 9 | 10 | /* 11 | * sessiongate.pdel 12 | * Deletes a payload of a session. 13 | * Returns OK. 14 | */ 15 | int PDelCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc); 16 | -------------------------------------------------------------------------------- /src/pget.c: -------------------------------------------------------------------------------- 1 | #include "pget.h" 2 | 3 | int PGetCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 4 | if (argc != 4) 5 | return RedisModule_WrongArity(ctx); 6 | RedisModule_AutoMemory(ctx); 7 | 8 | // Extract and validate it. 9 | size_t signKeyLen; 10 | const char *signKey = RedisModule_StringPtrLen(argv[1], &signKeyLen); 11 | if (signKeyLen == 0) 12 | return RedisModule_ReplyWithError( 13 | ctx, " must have at least one character"); 14 | 15 | // Extract and validate it. 16 | size_t tokenLen; 17 | const char *token = RedisModule_StringPtrLen(argv[2], &tokenLen); 18 | if (tokenLen == 0) 19 | return RedisModule_ReplyWithError( 20 | ctx, " must have at least one character"); 21 | else if (tokenLen != TOKEN_STRLEN) 22 | return RedisModule_ReplyWithError(ctx, " format is invalid"); 23 | 24 | // Parse the token. 25 | char tokenVersion[TOKEN_VERSION_STRLEN + 1]; 26 | char sessionId[SESSION_ID_STRLEN + 1]; 27 | char signature[SIGNATURE_STRLEN + 1]; 28 | parseToken(token, tokenVersion, sessionId, signature); 29 | 30 | // Recreate the signature of the session id and compare with the signature 31 | // contained in the token. 32 | char signatureCheck[SIGNATURE_STRLEN + 1]; 33 | signData((const unsigned char *)signKey, signKeyLen, 34 | (const unsigned char *)sessionId, SESSION_ID_STRLEN, signatureCheck); 35 | if (strncmp(signature, signatureCheck, SIGNATURE_STRLEN) != 0) 36 | return RedisModule_ReplyWithError( 37 | ctx, "the signature contained in is invalid"); 38 | 39 | // Check if the signature is the same stored in the session. 40 | RedisModuleString *sessionSignatureKeyStr = 41 | RedisModule_CreateStringPrintf(ctx, "sg-session:%s:signature", sessionId); 42 | RedisModuleKey *redisKey = RedisModule_OpenKey( 43 | ctx, sessionSignatureKeyStr, REDISMODULE_READ | REDISMODULE_WRITE); 44 | if (RedisModule_KeyType(redisKey) != REDISMODULE_KEYTYPE_STRING) { 45 | RedisModule_CloseKey(redisKey); 46 | return RedisModule_ReplyWithError( 47 | ctx, "the session id contained in does not exist"); 48 | } 49 | size_t signatureStoredLen; 50 | char *signatureStored = 51 | RedisModule_StringDMA(redisKey, &signatureStoredLen, REDISMODULE_READ); 52 | if (strncmp(signature, signatureStored, SIGNATURE_STRLEN) != 0) { 53 | RedisModule_CloseKey(redisKey); 54 | return RedisModule_ReplyWithError( 55 | ctx, "the signature contained in seems to be valid, but is " 56 | "different from the stored signature in the session"); 57 | } 58 | 59 | // Extract and validate it. 60 | const RedisModuleString *payloadName = argv[3]; 61 | size_t payloadNameLen; 62 | RedisModule_StringPtrLen(payloadName, &payloadNameLen); 63 | if (payloadNameLen == 0) 64 | return RedisModule_ReplyWithError( 65 | ctx, " must have at least one character"); 66 | else if (payloadNameLen > PAYLOAD_NAME_MAX_STRLEN) { 67 | char msg[128]; 68 | sprintf(msg, 69 | " length exceeds the maximum value allowed of %zu", 70 | PAYLOAD_NAME_MAX_STRLEN); 71 | return RedisModule_ReplyWithError(ctx, msg); 72 | } 73 | 74 | // Get the payload. 75 | RedisModuleString *sessionPayloadsKeyStr = 76 | RedisModule_CreateStringPrintf(ctx, "sg-session:%s:payloads", sessionId); 77 | redisKey = RedisModule_OpenKey(ctx, sessionPayloadsKeyStr, REDISMODULE_READ); 78 | if (RedisModule_KeyType(redisKey) != REDISMODULE_KEYTYPE_HASH) { 79 | RedisModule_CloseKey(redisKey); 80 | return RedisModule_ReplyWithError( 81 | ctx, "the requested does not exist"); 82 | } 83 | RedisModuleString *payloadData; 84 | RedisModule_HashGet(redisKey, REDISMODULE_HASH_NONE, payloadName, 85 | &payloadData, NULL); 86 | if (payloadData == NULL) { 87 | RedisModule_CloseKey(redisKey); 88 | return RedisModule_ReplyWithError( 89 | ctx, "the requested does not exist"); 90 | } 91 | RedisModule_CloseKey(redisKey); 92 | 93 | RedisModule_ReplyWithString(ctx, payloadData); 94 | return REDISMODULE_OK; 95 | } 96 | -------------------------------------------------------------------------------- /src/pget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "redismodule.h" 6 | 7 | #include "consts.h" 8 | #include "utils.h" 9 | 10 | /* 11 | * sessiongate.pget 12 | * Gets a payload of a session. 13 | * Returns . 14 | */ 15 | int PGetCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc); 16 | -------------------------------------------------------------------------------- /src/pset.c: -------------------------------------------------------------------------------- 1 | #include "pset.h" 2 | 3 | int PSetCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 4 | if (argc != 5) 5 | return RedisModule_WrongArity(ctx); 6 | RedisModule_AutoMemory(ctx); 7 | 8 | // Extract and validate it. 9 | size_t signKeyLen; 10 | const char *signKey = RedisModule_StringPtrLen(argv[1], &signKeyLen); 11 | if (signKeyLen == 0) 12 | return RedisModule_ReplyWithError( 13 | ctx, " must have at least one character"); 14 | 15 | // Extract and validate it. 16 | size_t tokenLen; 17 | const char *token = RedisModule_StringPtrLen(argv[2], &tokenLen); 18 | if (tokenLen == 0) 19 | return RedisModule_ReplyWithError( 20 | ctx, " must have at least one character"); 21 | else if (tokenLen != TOKEN_STRLEN) 22 | return RedisModule_ReplyWithError(ctx, " format is invalid"); 23 | 24 | // Parse the token. 25 | char tokenVersion[TOKEN_VERSION_STRLEN + 1]; 26 | char sessionId[SESSION_ID_STRLEN + 1]; 27 | char signature[SIGNATURE_STRLEN + 1]; 28 | parseToken(token, tokenVersion, sessionId, signature); 29 | 30 | // Recreate the signature of the session id and compare with the signature 31 | // contained in the token. 32 | char signatureCheck[SIGNATURE_STRLEN + 1]; 33 | signData((const unsigned char *)signKey, signKeyLen, 34 | (const unsigned char *)sessionId, SESSION_ID_STRLEN, signatureCheck); 35 | if (strncmp(signature, signatureCheck, SIGNATURE_STRLEN) != 0) 36 | return RedisModule_ReplyWithError( 37 | ctx, "the signature contained in is invalid"); 38 | 39 | // Check if the signature is the same stored in the session. 40 | RedisModuleString *sessionSignatureKeyStr = 41 | RedisModule_CreateStringPrintf(ctx, "sg-session:%s:signature", sessionId); 42 | RedisModuleKey *redisKey = RedisModule_OpenKey( 43 | ctx, sessionSignatureKeyStr, REDISMODULE_READ | REDISMODULE_WRITE); 44 | if (RedisModule_KeyType(redisKey) != REDISMODULE_KEYTYPE_STRING) { 45 | RedisModule_CloseKey(redisKey); 46 | return RedisModule_ReplyWithError( 47 | ctx, "the session id contained in does not exist"); 48 | } 49 | size_t signatureStoredLen; 50 | char *signatureStored = 51 | RedisModule_StringDMA(redisKey, &signatureStoredLen, REDISMODULE_READ); 52 | if (strncmp(signature, signatureStored, SIGNATURE_STRLEN) != 0) { 53 | RedisModule_CloseKey(redisKey); 54 | return RedisModule_ReplyWithError( 55 | ctx, "the signature contained in seems to be valid, but is " 56 | "different from the stored signature in the session"); 57 | } 58 | 59 | // Get the TTL of the session to assign it to the payload. 60 | mstime_t ttl = RedisModule_GetExpire(redisKey); 61 | 62 | RedisModule_CloseKey(redisKey); 63 | 64 | // Extract and validate it. 65 | const RedisModuleString *payloadName = argv[3]; 66 | size_t payloadNameLen; 67 | RedisModule_StringPtrLen(payloadName, &payloadNameLen); 68 | if (payloadNameLen == 0) 69 | return RedisModule_ReplyWithError( 70 | ctx, " must have at least one character"); 71 | else if (payloadNameLen > PAYLOAD_NAME_MAX_STRLEN) { 72 | char msg[128]; 73 | sprintf(msg, 74 | " length exceeds the maximum value allowed of %zu", 75 | PAYLOAD_NAME_MAX_STRLEN); 76 | return RedisModule_ReplyWithError(ctx, msg); 77 | } 78 | 79 | // Extract and validate it. 80 | const RedisModuleString *payloadData = argv[4]; 81 | size_t payloadDataLen; 82 | RedisModule_StringPtrLen(payloadData, &payloadDataLen); 83 | if (payloadDataLen == 0) 84 | return RedisModule_ReplyWithError( 85 | ctx, " must have at least one character"); 86 | else if (payloadDataLen > PAYLOAD_DATA_MAX_STRLEN) { 87 | char msg[128]; 88 | sprintf(msg, 89 | " length exceeds the maximum value allowed of %zu", 90 | PAYLOAD_DATA_MAX_STRLEN); 91 | return RedisModule_ReplyWithError(ctx, msg); 92 | } 93 | 94 | // Set the payload. 95 | RedisModuleString *sessionPayloadsKeyStr = 96 | RedisModule_CreateStringPrintf(ctx, "sg-session:%s:payloads", sessionId); 97 | redisKey = RedisModule_OpenKey(ctx, sessionPayloadsKeyStr, REDISMODULE_WRITE); 98 | RedisModule_HashSet(redisKey, REDISMODULE_HASH_NONE, payloadName, payloadData, 99 | NULL); 100 | 101 | // Set the TTL. 102 | if (ttl > 0) 103 | RedisModule_SetExpire(redisKey, ttl); 104 | 105 | RedisModule_CloseKey(redisKey); 106 | 107 | RedisModule_ReplyWithSimpleString(ctx, "OK"); 108 | return REDISMODULE_OK; 109 | } 110 | -------------------------------------------------------------------------------- /src/pset.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "redismodule.h" 6 | 7 | #include "consts.h" 8 | #include "utils.h" 9 | 10 | /* 11 | * sessiongate.pset 12 | * Sets a payload of a session. 13 | * Returns OK. 14 | */ 15 | int PSetCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc); 16 | -------------------------------------------------------------------------------- /src/start.c: -------------------------------------------------------------------------------- 1 | #include "start.h" 2 | 3 | int StartCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 4 | if (argc != 3) 5 | return RedisModule_WrongArity(ctx); 6 | RedisModule_AutoMemory(ctx); 7 | 8 | // Extract and validate it. 9 | size_t signKeyLen; 10 | const char *signKey = RedisModule_StringPtrLen(argv[1], &signKeyLen); 11 | if (signKeyLen == 0) 12 | return RedisModule_ReplyWithError( 13 | ctx, " must have at least one character"); 14 | 15 | // Extract and validate it. 16 | long long ttl; 17 | if (RedisModule_StringToLongLong(argv[2], &ttl) != REDISMODULE_OK) 18 | return RedisModule_ReplyWithError( 19 | ctx, " must be a valid integer that represents seconds"); 20 | if (ttl < 0) 21 | return RedisModule_ReplyWithError( 22 | ctx, " must be a valid integer that represents seconds"); 23 | 24 | char sessionId[SESSION_ID_STRLEN + 1] = {0}; 25 | RedisModuleString *sessionSignatureKeyStr; 26 | RedisModuleKey *redisKey; 27 | // A security measure to ensure no collisions will happen to existing session 28 | // IDs. Open a ticket and call me paranoid. You better like me being paranoid. 29 | while (1) { 30 | // Generate the session ID. 31 | generatePseudoRandomString(sessionId); 32 | sessionSignatureKeyStr = RedisModule_CreateStringPrintf( 33 | ctx, "sg-session:%s:signature", sessionId); 34 | 35 | // Verify if the session ID already exists. 36 | redisKey = 37 | RedisModule_OpenKey(ctx, sessionSignatureKeyStr, REDISMODULE_WRITE); 38 | if (RedisModule_KeyType(redisKey) == REDISMODULE_KEYTYPE_EMPTY) 39 | break; 40 | } 41 | 42 | char signature[SIGNATURE_STRLEN + 1]; 43 | signData((const unsigned char *)signKey, signKeyLen, 44 | (const unsigned char *)sessionId, SESSION_ID_STRLEN, signature); 45 | RedisModule_StringSet(redisKey, 46 | RedisModule_CreateStringPrintf(ctx, signature)); 47 | 48 | // Set the session TTL. 49 | RedisModule_SetExpire(redisKey, ttl * 1000); 50 | 51 | RedisModule_CloseKey(redisKey); 52 | 53 | RedisModule_ReplyWithString( 54 | ctx, RedisModule_CreateStringPrintf(ctx, "%s.%s.%s", TOKEN_VERSION, 55 | sessionId, signature)); 56 | return REDISMODULE_OK; 57 | } 58 | -------------------------------------------------------------------------------- /src/start.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "redismodule.h" 6 | 7 | #include "consts.h" 8 | #include "utils.h" 9 | 10 | /* 11 | * sessiongate.start 12 | * Starts a new session that will live for seconds. 13 | * Returns a token containing the token version, the session id and the signature. 14 | */ 15 | int StartCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc); 16 | -------------------------------------------------------------------------------- /src/utils.c: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | void generatePseudoRandomString(char *generatedStr) { 4 | uint8_t buf[SESSION_ID_STRLEN]; 5 | randombytes_buf(buf, SESSION_ID_STRLEN); 6 | for (uint16_t i = 0; i < SESSION_ID_STRLEN; i++) 7 | generatedStr[i] = 8 | SESSION_ID_DICTIONARY[buf[i] % SESSION_ID_DICTIONARY_STRLEN]; 9 | } 10 | 11 | void signData(const unsigned char *key, const size_t keyLen, 12 | const unsigned char *data, const size_t dataLen, 13 | char *signature) { 14 | unsigned char signatureHash[crypto_auth_hmacsha256_BYTES]; 15 | crypto_auth_hmacsha256_state state; 16 | crypto_auth_hmacsha256_init(&state, key, keyLen); 17 | crypto_auth_hmacsha256_update(&state, data, dataLen); 18 | crypto_auth_hmacsha256_final(&state, signatureHash); 19 | sodium_bin2hex(signature, SIGNATURE_STRLEN + 1, signatureHash, 20 | 32); // 32 bytes = 256 bits. 21 | } 22 | 23 | void parseToken(const char *token, char *tokenVersion, char *sessionId, 24 | char *signature) { 25 | uint8_t i = 0; 26 | uint8_t ti = 0; 27 | for (i = 0; i < TOKEN_VERSION_STRLEN; i++, ti++) 28 | tokenVersion[i] = token[ti]; 29 | tokenVersion[TOKEN_VERSION_STRLEN] = '\0'; 30 | ti++; 31 | for (i = 0; i < SESSION_ID_STRLEN; i++, ti++) 32 | sessionId[i] = token[ti]; 33 | sessionId[SESSION_ID_STRLEN] = '\0'; 34 | ti++; 35 | for (i = 0; i < SIGNATURE_STRLEN; i++, ti++) 36 | signature[i] = token[ti]; 37 | signature[SIGNATURE_STRLEN] = '\0'; 38 | } 39 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "sodium.h" 6 | 7 | #include "consts.h" 8 | 9 | void generatePseudoRandomString(char *generatedStr); 10 | void signData(const unsigned char *key, const size_t keyLen, const unsigned char *data, const size_t dataLen, char *signature); 11 | void parseToken(const char *token, char *tokenVersion, char *sessionId, char *signature); 12 | -------------------------------------------------------------------------------- /tests/integration/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_test") 2 | 3 | go_test( 4 | name = "go_default_test", 5 | size = "small", 6 | srcs = [ 7 | "integration_suite_test.go", 8 | "integration_test.go", 9 | ], 10 | data = [ 11 | "//:sessiongate.so", 12 | ], 13 | deps = [ 14 | "@com_github_onsi_ginkgo//:go_default_library", 15 | "@com_github_onsi_gomega//:go_default_library", 16 | ], 17 | ) 18 | -------------------------------------------------------------------------------- /tests/integration/integration_suite_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os/exec" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | const ( 18 | unixSocket = "redis.sock" 19 | modulePath = "../../sessiongate.so" 20 | moduleLoadedMessage = "Module 'sessiongate' loaded" 21 | serverReadyMessage = "The server is now ready to accept connections at " + unixSocket 22 | ) 23 | 24 | var redisCmd *exec.Cmd 25 | 26 | func TestIntegration(t *testing.T) { 27 | RegisterFailHandler(Fail) 28 | RunSpecs(t, "Integration Suite") 29 | } 30 | 31 | var _ = BeforeSuite(func() { 32 | redisCmd = exec.Command( 33 | "redis-server", 34 | "--port", "0", // Disable TCP. 35 | "--unixsocket", unixSocket, 36 | "--loadmodule", modulePath, 37 | ) 38 | stdoutReader, stdoutWriter := io.Pipe() 39 | redisCmd.Stdout = stdoutWriter 40 | err := redisCmd.Start() 41 | Expect(err).NotTo(HaveOccurred()) 42 | started := make(chan struct{}) 43 | timeout := time.After(time.Second * 3) 44 | go func() { 45 | scanner := bufio.NewScanner(stdoutReader) 46 | moduleLoaded := false 47 | serverReady := false 48 | for scanner.Scan() { 49 | line := scanner.Text() 50 | fmt.Println(line) 51 | if strings.Contains(line, moduleLoadedMessage) { 52 | moduleLoaded = true 53 | } 54 | if strings.Contains(line, serverReadyMessage) { 55 | serverReady = true 56 | } 57 | if moduleLoaded && serverReady { 58 | close(started) 59 | } 60 | } 61 | err = scanner.Err() 62 | Expect(err).NotTo(HaveOccurred()) 63 | }() 64 | select { 65 | case <-started: 66 | return 67 | case <-timeout: 68 | err := fmt.Errorf("something went wrong while starting Redis server with sessiongate.so module") 69 | log.Fatal(err) 70 | } 71 | }) 72 | 73 | var _ = AfterSuite(func() { 74 | err := redisCmd.Process.Kill() 75 | Expect(err).NotTo(HaveOccurred()) 76 | redisCmd.Wait() 77 | }) 78 | -------------------------------------------------------------------------------- /tests/integration/integration_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os/exec" 7 | "strings" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | const ( 14 | rediscliCmd = "redis-cli" 15 | signKey = "mysecret12345" 16 | tokenRegexp = `^v1\.[0-9a-zA-Z]{16}\.[0-9a-f]{64}$` 17 | tokenTTL = "10" 18 | maxPayloadName = 200 19 | maxPayloadData = int(1e6 * 8) 20 | startCmd = "sessiongate.start" 21 | endCmd = "sessiongate.end" 22 | psetCmd = "sessiongate.pset" 23 | pgetCmd = "sessiongate.pget" 24 | pdelCmd = "sessiongate.pdel" 25 | expireCmd = "sessiongate.expire" 26 | ) 27 | 28 | var ( 29 | fakeWhateverToken = fmt.Sprintf("v1.%s.%s", strings.Repeat("a", 16), strings.Repeat("b", 64)) 30 | fakeInvalidTokenFormat = "" 31 | fakeInvalidTokenSignature = fmt.Sprintf("v1.%s.%s", strings.Repeat("a", 16), strings.Repeat("b", 64)) 32 | ) 33 | 34 | var _ = Describe("Integration", func() { 35 | Describe(startCmd, func() { 36 | const cmd = startCmd 37 | 38 | itShouldFailWithWrongNumberOfArguments(cmd, signKey) 39 | itShouldFailWithWrongNumberOfArguments(cmd, signKey, tokenTTL, "extra") 40 | itShouldFailWithEmptySignKey(cmd, "", tokenTTL) 41 | It("should fail with invalid ", func() { 42 | out := rediscli(cmd, signKey, "") 43 | err := " must be a valid integer that represents seconds" 44 | Expect(out).To(Equal(err)) 45 | }) 46 | It("should fail with invalid ", func() { 47 | out := rediscli(cmd, signKey, "asd") 48 | err := " must be a valid integer that represents seconds" 49 | Expect(out).To(Equal(err)) 50 | }) 51 | It("should fail with invalid ", func() { 52 | out := rediscli(cmd, signKey, "-1") 53 | err := " must be a valid integer that represents seconds" 54 | Expect(out).To(Equal(err)) 55 | }) 56 | It("should succeed", func() { 57 | mustToken() 58 | }) 59 | }) 60 | 61 | Describe(endCmd, func() { 62 | const cmd = endCmd 63 | 64 | itShouldFailWithWrongNumberOfArguments(cmd, signKey) 65 | itShouldFailWithWrongNumberOfArguments(cmd, signKey, fakeWhateverToken, "extra") 66 | itShouldFailWithEmptySignKey(cmd, "", fakeWhateverToken) 67 | itShouldFailWithEmptyToken(cmd, signKey, "") 68 | itShouldFailWithInvalidTokenFormat(cmd, signKey, fakeInvalidTokenFormat) 69 | itShouldFailWithInvalidTokenSignature(cmd, signKey, fakeInvalidTokenSignature) 70 | It("should fail when trying to end an already ended token", func() { 71 | token := mustToken() 72 | mustEnd(token) 73 | out := rediscli(cmd, signKey, token) 74 | err := "the session id contained in does not exist" 75 | Expect(out).To(Equal(err)) 76 | }) 77 | It("should succeed", func() { 78 | token := mustToken() 79 | mustEnd(token) 80 | }) 81 | }) 82 | 83 | Describe(psetCmd, func() { 84 | const cmd = psetCmd 85 | const payloadName = "someName" 86 | const payloadData = "someData" 87 | 88 | itShouldFailWithWrongNumberOfArguments(cmd, signKey, fakeWhateverToken, payloadName) 89 | itShouldFailWithWrongNumberOfArguments(cmd, signKey, fakeWhateverToken, payloadName, payloadData, "extra") 90 | itShouldFailWithEmptySignKey(cmd, "", fakeWhateverToken, payloadName, payloadData) 91 | itShouldFailWithEmptyToken(cmd, signKey, "", payloadName, payloadData) 92 | itShouldFailWithInvalidTokenFormat(cmd, signKey, fakeInvalidTokenFormat, payloadName, payloadData) 93 | itShouldFailWithInvalidTokenSignature(cmd, signKey, fakeInvalidTokenSignature, payloadName, payloadData) 94 | It("should fail when trying to set a payload on an already ended token", func() { 95 | token := mustToken() 96 | mustEnd(token) 97 | out := rediscli(cmd, signKey, token, payloadName, payloadData) 98 | err := "the session id contained in does not exist" 99 | Expect(out).To(Equal(err)) 100 | }) 101 | It("should fail with empty ", func() { 102 | out := rediscli(cmd, signKey, mustToken(), "", payloadData) 103 | err := " must have at least one character" 104 | Expect(out).To(Equal(err)) 105 | }) 106 | It("should fail with max length exceeded", func() { 107 | over := strings.Repeat("a", maxPayloadName+1) 108 | out := rediscli(cmd, signKey, mustToken(), over, payloadData) 109 | err := fmt.Sprintf(" length exceeds the maximum value allowed of %d", maxPayloadName) 110 | Expect(out).To(Equal(err)) 111 | }) 112 | It("should fail with empty ", func() { 113 | out := rediscli(cmd, signKey, mustToken(), payloadName, "") 114 | err := " must have at least one character" 115 | Expect(out).To(Equal(err)) 116 | }) 117 | It("should fail with max size exceeded", func() { 118 | over := strings.Repeat("a", maxPayloadData+1) 119 | out := rediscli(cmd, signKey, mustToken(), payloadName, over) 120 | err := fmt.Sprintf(" length exceeds the maximum value allowed of %d", maxPayloadData) 121 | Expect(out).To(Equal(err)) 122 | }) 123 | It("should succeed", func() { 124 | mustSet(mustToken(), payloadName, payloadData) 125 | }) 126 | }) 127 | 128 | Describe(pgetCmd, func() { 129 | const cmd = pgetCmd 130 | const payloadName = "someName" 131 | const payloadData = "someData" 132 | 133 | itShouldFailWithWrongNumberOfArguments(cmd, signKey, fakeWhateverToken) 134 | itShouldFailWithWrongNumberOfArguments(cmd, signKey, fakeWhateverToken, payloadName, "extra") 135 | itShouldFailWithEmptySignKey(cmd, "", fakeWhateverToken, payloadName) 136 | itShouldFailWithEmptyToken(cmd, signKey, "", payloadName) 137 | itShouldFailWithInvalidTokenFormat(cmd, signKey, fakeInvalidTokenFormat, payloadName) 138 | itShouldFailWithInvalidTokenSignature(cmd, signKey, fakeInvalidTokenSignature, payloadName) 139 | It("should fail when trying to get a payload on an already ended token", func() { 140 | token := mustToken() 141 | mustEnd(token) 142 | out := rediscli(cmd, signKey, token, payloadName) 143 | err := "the session id contained in does not exist" 144 | Expect(out).To(Equal(err)) 145 | }) 146 | It("should fail with empty ", func() { 147 | out := rediscli(cmd, signKey, mustToken(), "") 148 | err := " must have at least one character" 149 | Expect(out).To(Equal(err)) 150 | }) 151 | It("should fail with max length exceeded", func() { 152 | over := strings.Repeat("a", maxPayloadName+1) 153 | out := rediscli(cmd, signKey, mustToken(), over) 154 | err := fmt.Sprintf(" length exceeds the maximum value allowed of %d", maxPayloadName) 155 | Expect(out).To(Equal(err)) 156 | }) 157 | It("should fail with non-existent ", func() { 158 | out := rediscli(cmd, signKey, mustToken(), payloadName) 159 | err := "the requested does not exist" 160 | Expect(out).To(Equal(err)) 161 | }) 162 | It("should succeed setting and mutating multiple payloads", func() { 163 | token := mustToken() 164 | for i := 1; i < 10; i++ { 165 | pn := fmt.Sprintf("%s_%d", payloadName, i) 166 | pd := fmt.Sprintf("%s_%d", payloadData, i) 167 | mustSet(token, pn, pd) 168 | out := rediscli(cmd, signKey, token, pn) 169 | Expect(out).To(Equal(pd)) 170 | anotherData := fmt.Sprintf("anotherData_%d", i) 171 | mustSet(token, pn, anotherData) 172 | out = rediscli(cmd, signKey, token, pn) 173 | Expect(out).To(Equal(anotherData)) 174 | } 175 | }) 176 | }) 177 | 178 | Describe(pdelCmd, func() { 179 | const cmd = pdelCmd 180 | const payloadName = "someName" 181 | const payloadData = "someData" 182 | 183 | itShouldFailWithWrongNumberOfArguments(cmd, signKey, fakeWhateverToken) 184 | itShouldFailWithWrongNumberOfArguments(cmd, signKey, fakeWhateverToken, payloadName, "extra") 185 | itShouldFailWithEmptySignKey(cmd, "", fakeWhateverToken, payloadName) 186 | itShouldFailWithEmptyToken(cmd, signKey, "", payloadName) 187 | itShouldFailWithInvalidTokenFormat(cmd, signKey, fakeInvalidTokenFormat, payloadName) 188 | itShouldFailWithInvalidTokenSignature(cmd, signKey, fakeInvalidTokenSignature, payloadName) 189 | It("should fail when trying to delete a payload on an already ended token", func() { 190 | token := mustToken() 191 | mustEnd(token) 192 | out := rediscli(cmd, signKey, token, payloadName) 193 | err := "the session id contained in does not exist" 194 | Expect(out).To(Equal(err)) 195 | }) 196 | It("should fail with empty ", func() { 197 | out := rediscli(cmd, signKey, mustToken(), "") 198 | err := " must have at least one character" 199 | Expect(out).To(Equal(err)) 200 | }) 201 | It("should fail with max length exceeded", func() { 202 | over := strings.Repeat("a", maxPayloadName+1) 203 | out := rediscli(cmd, signKey, mustToken(), over) 204 | err := fmt.Sprintf(" length exceeds the maximum value allowed of %d", maxPayloadName) 205 | Expect(out).To(Equal(err)) 206 | }) 207 | It("should fail with non-existent ", func() { 208 | out := rediscli(cmd, signKey, mustToken(), payloadName) 209 | err := "the requested does not exist" 210 | Expect(out).To(Equal(err)) 211 | }) 212 | It("should succeed setting and deleting multiple payloads", func() { 213 | token := mustToken() 214 | for i := 1; i < 10; i++ { 215 | pn := fmt.Sprintf("%s_%d", payloadName, i) 216 | pd := fmt.Sprintf("%s_%d", payloadData, i) 217 | mustSet(token, pn, pd) 218 | mustDel(token, pn) 219 | out := rediscli(pgetCmd, signKey, token, pn) 220 | err := "the requested does not exist" 221 | Expect(out).To(Equal(err)) 222 | } 223 | }) 224 | }) 225 | 226 | Describe(expireCmd, func() { 227 | const cmd = expireCmd 228 | const newTTL = "500" 229 | 230 | itShouldFailWithWrongNumberOfArguments(cmd, signKey, fakeWhateverToken) 231 | itShouldFailWithWrongNumberOfArguments(cmd, signKey, fakeWhateverToken, newTTL, "extra") 232 | itShouldFailWithEmptySignKey(cmd, "", fakeWhateverToken, newTTL) 233 | itShouldFailWithEmptyToken(cmd, signKey, "", newTTL) 234 | itShouldFailWithInvalidTokenFormat(cmd, signKey, fakeInvalidTokenFormat, newTTL) 235 | itShouldFailWithInvalidTokenSignature(cmd, signKey, fakeInvalidTokenSignature, newTTL) 236 | It("should fail with invalid ", func() { 237 | token := mustToken() 238 | out := rediscli(cmd, signKey, token, "") 239 | err := " must be a valid integer that represents seconds" 240 | Expect(out).To(Equal(err)) 241 | }) 242 | It("should fail with invalid ", func() { 243 | token := mustToken() 244 | out := rediscli(cmd, signKey, token, "asd") 245 | err := " must be a valid integer that represents seconds" 246 | Expect(out).To(Equal(err)) 247 | }) 248 | It("should fail with invalid ", func() { 249 | token := mustToken() 250 | out := rediscli(cmd, signKey, token, "-1") 251 | err := " must be a valid integer that represents seconds" 252 | Expect(out).To(Equal(err)) 253 | }) 254 | It("should fail when trying to delete a payload on an already ended token", func() { 255 | token := mustToken() 256 | mustEnd(token) 257 | out := rediscli(cmd, signKey, token, newTTL) 258 | err := "the session id contained in does not exist" 259 | Expect(out).To(Equal(err)) 260 | }) 261 | It("should succeed", func() { 262 | out := rediscli(cmd, signKey, mustToken(), newTTL) 263 | Expect(out).To(Equal("OK")) 264 | }) 265 | }) 266 | }) 267 | 268 | func itShouldFailWithWrongNumberOfArguments(redisCmd string, redisArgs ...string) { 269 | It("should fail with wrong number of arguments", func() { 270 | out := rediscli(redisCmd, redisArgs...) 271 | err := fmt.Sprintf("ERR wrong number of arguments for '%s' command", redisCmd) 272 | Expect(out).To(Equal(err)) 273 | }) 274 | } 275 | 276 | func itShouldFailWithEmptySignKey(redisCmd string, redisArgs ...string) { 277 | It("should fail with empty ", func() { 278 | out := rediscli(redisCmd, redisArgs...) 279 | err := " must have at least one character" 280 | Expect(out).To(Equal(err)) 281 | }) 282 | } 283 | 284 | func itShouldFailWithEmptyToken(redisCmd string, redisArgs ...string) { 285 | It("should fail with empty ", func() { 286 | out := rediscli(redisCmd, redisArgs...) 287 | err := " must have at least one character" 288 | Expect(out).To(Equal(err)) 289 | }) 290 | } 291 | 292 | func itShouldFailWithInvalidTokenFormat(redisCmd string, redisArgs ...string) { 293 | It("should fail with invalid format", func() { 294 | out := rediscli(redisCmd, redisArgs...) 295 | err := " format is invalid" 296 | Expect(out).To(Equal(err)) 297 | }) 298 | } 299 | 300 | func itShouldFailWithInvalidTokenSignature(redisCmd string, redisArgs ...string) { 301 | It("should fail with invalid signature", func() { 302 | out := rediscli(redisCmd, redisArgs...) 303 | err := "the signature contained in is invalid" 304 | Expect(out).To(Equal(err)) 305 | }) 306 | } 307 | 308 | func mustToken() string { 309 | token := rediscli(startCmd, signKey, tokenTTL) 310 | Expect(token).To(MatchRegexp(tokenRegexp)) 311 | return token 312 | } 313 | 314 | func mustEnd(token string) { 315 | out := rediscli(endCmd, signKey, token) 316 | Expect(out).To(Equal("OK")) 317 | } 318 | 319 | func mustSet(token, payloadName, payloadData string) { 320 | out := rediscli(psetCmd, signKey, token, payloadName, payloadData) 321 | Expect(out).To(Equal("OK")) 322 | } 323 | 324 | func mustDel(token, payloadName string) { 325 | out := rediscli(pdelCmd, signKey, token, payloadName) 326 | Expect(out).To(Equal("OK")) 327 | } 328 | 329 | func rediscli(redisCmd string, redisArgs ...string) string { 330 | cmd := exec.Command(rediscliCmd, "-s", unixSocket) 331 | stdinReader, stdinWriter := io.Pipe() 332 | go func() { 333 | defer stdinWriter.Close() 334 | fmt.Fprintf(stdinWriter, "%s ", redisCmd) 335 | for _, arg := range redisArgs { 336 | fmt.Fprintf(stdinWriter, "'%s' ", arg) 337 | } 338 | }() 339 | var combinedOut strings.Builder 340 | cmd.Stdin = stdinReader 341 | cmd.Stdout = &combinedOut 342 | cmd.Stderr = &combinedOut 343 | err := cmd.Run() 344 | Expect(err).NotTo(HaveOccurred()) 345 | return strings.TrimSpace(combinedOut.String()) 346 | } 347 | --------------------------------------------------------------------------------