├── .gitignore ├── Java ├── README.md └── Token.java ├── LICENSE.txt ├── Node ├── JavaScript │ ├── .editorconfig │ ├── README.md │ ├── browser.js │ ├── index.js │ ├── package.json │ └── yarn.lock └── TypeScript │ ├── .babelrc │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .prettierrc.js │ ├── README.md │ ├── package.json │ ├── src │ └── index.ts │ ├── tsconfig.json │ ├── webpack.config.js │ └── yarn.lock ├── README-en.md ├── README.md ├── csharp ├── .gitignore ├── Netless │ └── Token.cs ├── Program.cs ├── README.md ├── csharp.csproj └── csharp.sln ├── golang ├── README.md ├── go.mod ├── go.sum ├── token.go └── token_test.go ├── php ├── .editorconfig ├── .php_cs ├── README.md ├── composer.json ├── phpstan.neon ├── src │ └── Netless │ │ └── Token │ │ └── Generate.php └── tests │ └── TokenTest.php ├── python ├── README.md └── netless_token.py └── ruby ├── .editorconfig ├── Gemfile ├── Gemfile.lock ├── README.md └── lib └── token.rb /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS template 2 | # General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must end with two \r 8 | Icon 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | .com.apple.timemachine.donotpresent 21 | 22 | # Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | ### Vim template 30 | # Swap 31 | [._]*.s[a-v][a-z] 32 | !*.svg # comment out if you don't need vector files 33 | [._]*.sw[a-p] 34 | [._]s[a-rt-v][a-z] 35 | [._]ss[a-gi-z] 36 | [._]sw[a-p] 37 | 38 | # Session 39 | Session.vim 40 | Sessionx.vim 41 | 42 | # Temporary 43 | .netrwhist 44 | *~ 45 | # Auto-generated tag files 46 | tags 47 | # Persistent undo 48 | [._]*.un~ 49 | 50 | ### C template 51 | # Prerequisites 52 | *.d 53 | 54 | # Object files 55 | *.o 56 | *.ko 57 | *.obj 58 | *.elf 59 | 60 | # Linker output 61 | *.ilk 62 | *.map 63 | *.exp 64 | 65 | # Precompiled Headers 66 | *.gch 67 | *.pch 68 | 69 | # Libraries 70 | *.lib 71 | *.a 72 | *.la 73 | *.lo 74 | 75 | # Shared objects (inc. Windows DLLs) 76 | *.dll 77 | *.so 78 | *.so.* 79 | *.dylib 80 | 81 | # Executables 82 | *.exe 83 | *.out 84 | *.app 85 | *.i*86 86 | *.x86_64 87 | *.hex 88 | 89 | # Debug files 90 | *.dSYM/ 91 | *.su 92 | *.idb 93 | *.pdb 94 | 95 | # Kernel Module Compile Results 96 | *.cmd 97 | .tmp_versions/ 98 | modules.order 99 | Module.symvers 100 | Mkfile.old 101 | dkms.conf 102 | 103 | ### Composer template 104 | composer.phar 105 | /vendor/ 106 | 107 | # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control 108 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 109 | # composer.lock 110 | 111 | ### Node template 112 | # Logs 113 | logs 114 | *.log 115 | npm-debug.log* 116 | yarn-debug.log* 117 | yarn-error.log* 118 | lerna-debug.log* 119 | 120 | # Diagnostic reports (https://nodejs.org/api/report.html) 121 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 122 | 123 | # Runtime data 124 | pids 125 | *.pid 126 | *.seed 127 | *.pid.lock 128 | 129 | # Directory for instrumented libs generated by jscoverage/JSCover 130 | lib-cov 131 | 132 | # Coverage directory used by tools like istanbul 133 | coverage 134 | *.lcov 135 | 136 | # nyc test coverage 137 | .nyc_output 138 | 139 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 140 | .grunt 141 | 142 | # Bower dependency directory (https://bower.io/) 143 | bower_components 144 | 145 | # node-waf configuration 146 | .lock-wscript 147 | 148 | # Compiled binary addons (https://nodejs.org/api/addons.html) 149 | build/Release 150 | 151 | # Dependency directories 152 | node_modules/ 153 | jspm_packages/ 154 | 155 | # Snowpack dependency directory (https://snowpack.dev/) 156 | web_modules/ 157 | 158 | # TypeScript cache 159 | *.tsbuildinfo 160 | 161 | # Optional npm cache directory 162 | .npm 163 | 164 | # Optional eslint cache 165 | .eslintcache 166 | 167 | # Microbundle cache 168 | .rpt2_cache/ 169 | .rts2_cache_cjs/ 170 | .rts2_cache_es/ 171 | .rts2_cache_umd/ 172 | 173 | # Optional REPL history 174 | .node_repl_history 175 | 176 | # Output of 'npm pack' 177 | *.tgz 178 | 179 | # Yarn Integrity file 180 | .yarn-integrity 181 | 182 | # dotenv environment variables file 183 | .env 184 | .env.test 185 | 186 | # parcel-bundler cache (https://parceljs.org/) 187 | .cache 188 | .parcel-cache 189 | 190 | # Next.js build output 191 | .next 192 | out 193 | 194 | # Nuxt.js build / generate output 195 | .nuxt 196 | dist 197 | 198 | # Gatsby files 199 | .cache/ 200 | # Comment in the public line in if your project uses Gatsby and not Next.js 201 | # https://nextjs.org/blog/next-9-1#public-directory-support 202 | # public 203 | 204 | # vuepress build output 205 | .vuepress/dist 206 | 207 | # Serverless directories 208 | .serverless/ 209 | 210 | # FuseBox cache 211 | .fusebox/ 212 | 213 | # DynamoDB Local files 214 | .dynamodb/ 215 | 216 | # TernJS port file 217 | .tern-port 218 | 219 | # Stores VSCode versions used for testing VSCode extensions 220 | .vscode-test 221 | 222 | # yarn v2 223 | .yarn/cache 224 | .yarn/unplugged 225 | .yarn/build-state.yml 226 | .yarn/install-state.gz 227 | .pnp.* 228 | 229 | ### Go template 230 | # Binaries for programs and plugins 231 | *.exe 232 | *.exe~ 233 | *.dll 234 | *.so 235 | *.dylib 236 | 237 | # Test binary, built with `go test -c` 238 | *.test 239 | 240 | # Output of the go coverage tool, specifically when used with LiteIDE 241 | *.out 242 | 243 | # Dependency directories (remove the comment below to include it) 244 | # vendor/ 245 | 246 | ### Ruby template 247 | *.gem 248 | *.rbc 249 | /.config 250 | /coverage/ 251 | /InstalledFiles 252 | /pkg/ 253 | /spec/reports/ 254 | /spec/examples.txt 255 | /test/tmp/ 256 | /test/version_tmp/ 257 | /tmp/ 258 | 259 | # Used by dotenv library to load environment variables. 260 | # .env 261 | 262 | # Ignore Byebug command history file. 263 | .byebug_history 264 | 265 | ## Specific to RubyMotion: 266 | .dat* 267 | .repl_history 268 | build/ 269 | *.bridgesupport 270 | build-iPhoneOS/ 271 | build-iPhoneSimulator/ 272 | 273 | ## Specific to RubyMotion (use of CocoaPods): 274 | # 275 | # We recommend against adding the Pods directory to your .gitignore. However 276 | # you should judge for yourself, the pros and cons are mentioned at: 277 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 278 | # 279 | # vendor/Pods/ 280 | 281 | ## Documentation cache and generated files: 282 | /.yardoc/ 283 | /_yardoc/ 284 | /doc/ 285 | /rdoc/ 286 | 287 | ## Environment normalization: 288 | /.bundle/ 289 | /vendor/bundle 290 | /lib/bundler/man/ 291 | 292 | # for a library or gem, you might want to ignore these files since the code is 293 | # intended to run in multiple environments; otherwise, check them in: 294 | # Gemfile.lock 295 | # .ruby-version 296 | # .ruby-gemset 297 | 298 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 299 | .rvmrc 300 | 301 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 302 | # .rubocop-https?--* 303 | 304 | ### C++ template 305 | # Prerequisites 306 | *.d 307 | 308 | # Compiled Object files 309 | *.slo 310 | *.lo 311 | *.o 312 | *.obj 313 | 314 | # Precompiled Headers 315 | *.gch 316 | *.pch 317 | 318 | # Compiled Dynamic libraries 319 | *.so 320 | *.dylib 321 | *.dll 322 | 323 | # Fortran module files 324 | *.smod 325 | 326 | # Compiled Static libraries 327 | *.lai 328 | *.la 329 | *.a 330 | *.lib 331 | 332 | # Executables 333 | *.exe 334 | *.out 335 | *.app 336 | 337 | ### Windows template 338 | # Windows thumbnail cache files 339 | Thumbs.db 340 | Thumbs.db:encryptable 341 | ehthumbs.db 342 | ehthumbs_vista.db 343 | 344 | # Dump file 345 | *.stackdump 346 | 347 | # Folder config file 348 | [Dd]esktop.ini 349 | 350 | # Recycle Bin used on file shares 351 | $RECYCLE.BIN/ 352 | 353 | # Windows Installer files 354 | *.cab 355 | *.msi 356 | *.msix 357 | *.msm 358 | *.msp 359 | 360 | # Windows shortcuts 361 | *.lnk 362 | 363 | ### Python template 364 | # Byte-compiled / optimized / DLL files 365 | __pycache__/ 366 | *.py[cod] 367 | *$py.class 368 | 369 | # C extensions 370 | *.so 371 | 372 | # Distribution / packaging 373 | .Python 374 | build/ 375 | develop-eggs/ 376 | dist/ 377 | downloads/ 378 | eggs/ 379 | .eggs/ 380 | lib64/ 381 | parts/ 382 | sdist/ 383 | var/ 384 | wheels/ 385 | share/python-wheels/ 386 | *.egg-info/ 387 | .installed.cfg 388 | *.egg 389 | MANIFEST 390 | 391 | # PyInstaller 392 | # Usually these files are written by a python script from a template 393 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 394 | *.manifest 395 | *.spec 396 | 397 | # Installer logs 398 | pip-log.txt 399 | pip-delete-this-directory.txt 400 | 401 | # Unit test / coverage reports 402 | htmlcov/ 403 | .tox/ 404 | .nox/ 405 | .coverage 406 | .coverage.* 407 | .cache 408 | nosetests.xml 409 | coverage.xml 410 | *.cover 411 | *.py,cover 412 | .hypothesis/ 413 | .pytest_cache/ 414 | cover/ 415 | 416 | # Translations 417 | *.mo 418 | *.pot 419 | 420 | # Django stuff: 421 | *.log 422 | local_settings.py 423 | db.sqlite3 424 | db.sqlite3-journal 425 | 426 | # Flask stuff: 427 | instance/ 428 | .webassets-cache 429 | 430 | # Scrapy stuff: 431 | .scrapy 432 | 433 | # Sphinx documentation 434 | docs/_build/ 435 | 436 | # PyBuilder 437 | .pybuilder/ 438 | target/ 439 | 440 | # Jupyter Notebook 441 | .ipynb_checkpoints 442 | 443 | # IPython 444 | profile_default/ 445 | ipython_config.py 446 | 447 | # pyenv 448 | # For a library or package, you might want to ignore these files since the code is 449 | # intended to run in multiple environments; otherwise, check them in: 450 | # .python-version 451 | 452 | # pipenv 453 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 454 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 455 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 456 | # install all needed dependencies. 457 | #Pipfile.lock 458 | 459 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 460 | __pypackages__/ 461 | 462 | # Celery stuff 463 | celerybeat-schedule 464 | celerybeat.pid 465 | 466 | # SageMath parsed files 467 | *.sage.py 468 | 469 | # Environments 470 | .env 471 | .venv 472 | env/ 473 | venv/ 474 | ENV/ 475 | env.bak/ 476 | venv.bak/ 477 | 478 | # Spyder project settings 479 | .spyderproject 480 | .spyproject 481 | 482 | # Rope project settings 483 | .ropeproject 484 | 485 | # mkdocs documentation 486 | /site 487 | 488 | # mypy 489 | .mypy_cache/ 490 | .dmypy.json 491 | dmypy.json 492 | 493 | # Pyre type checker 494 | .pyre/ 495 | 496 | # pytype static type analyzer 497 | .pytype/ 498 | 499 | # Cython debug symbols 500 | cython_debug/ 501 | 502 | ### Java template 503 | # Compiled class file 504 | *.class 505 | 506 | # Log file 507 | *.log 508 | 509 | # BlueJ files 510 | *.ctxt 511 | 512 | # Mobile Tools for Java (J2ME) 513 | .mtj.tmp/ 514 | 515 | # Package Files # 516 | *.jar 517 | *.war 518 | *.nar 519 | *.ear 520 | *.zip 521 | *.tar.gz 522 | *.rar 523 | 524 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 525 | hs_err_pid* 526 | 527 | # PHP 528 | *.cache 529 | php/vendor 530 | php/composer.lock 531 | 532 | # IDE 533 | .idea/ 534 | .vscode/ -------------------------------------------------------------------------------- /Java/README.md: -------------------------------------------------------------------------------- 1 | ## JAVA 2 | 3 | > Token.java 可以在其中加入 main 函数后,执行 javac Token.java,再执行 java Token 直接运行。 4 | 5 | ### 使用实例 6 | 7 | #### SDK Token 8 | 9 | ```java 10 | Map map = new HashMap<>(); 11 | map.put("role", Token.TokenRole.Admin.getValue()); // 可以选择 TokenRole.Reader / TokenRole.Writer 12 | 13 | String sdkToken = Token.sdkToken("netless ak", 14 | "netless sk", 15 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 16 | map); 17 | ``` 18 | 19 | #### Room Token 20 | 21 | ```java 22 | Map map = new HashMap<>(); 23 | map.put("role", Token.TokenRole.Reader.getValue()); // 可以选择 TokenRole.Admin / TokenRole.Writer 24 | map.put("uuid", "房间的 UUID"); 25 | 26 | String roomToken = Token.roomToken("netless ak", 27 | "netless sk", 28 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 29 | map); 30 | ``` 31 | 32 | #### Task Token 33 | 34 | ```java 35 | Map map = new HashMap<>(); 36 | map.put("role", Token.TokenRole.Writer.getValue()); // 可以选择 TokenRole.Reader / TokenRole.Admin 37 | map.put("uuid", "任务的 UUID"); 38 | 39 | String taskToken = Token.taskToken("netless ak", 40 | "netless sk", 41 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 42 | map); 43 | ``` 44 | -------------------------------------------------------------------------------- /Java/Token.java: -------------------------------------------------------------------------------- 1 | import javax.crypto.Mac; 2 | import javax.crypto.spec.SecretKeySpec; 3 | import java.io.UnsupportedEncodingException; 4 | import java.net.URLEncoder; 5 | import java.util.*; 6 | 7 | public class Token { 8 | public enum TokenPrefix { 9 | SDK("NETLESSSDK_"), 10 | ROOM("NETLESSROOM_"), 11 | TASK("NETLESSTASK_"); 12 | 13 | private String value; 14 | TokenPrefix(String name) { 15 | this.value = name; 16 | } 17 | public String getValue() { 18 | return value; 19 | } 20 | } 21 | 22 | /** 23 | * 数字越小,权限越大 24 | */ 25 | public enum TokenRole { 26 | Admin("0"), 27 | Writer("1"), 28 | Reader("2"); 29 | 30 | private String value; 31 | TokenRole(String name) { 32 | this.value = name; 33 | } 34 | public String getValue() { 35 | return value; 36 | } 37 | } 38 | 39 | public static String sdkToken(String accessKey, String secretAccessKey, long lifespan, Map content) throws Exception { 40 | return createToken(TokenPrefix.SDK.getValue(), accessKey, secretAccessKey, lifespan, content); 41 | } 42 | 43 | public static String roomToken(String accessKey, String secretAccessKey, long lifespan, Map content) throws Exception { 44 | return createToken(TokenPrefix.ROOM.getValue(), accessKey, secretAccessKey, lifespan, content); 45 | } 46 | 47 | public static String taskToken(String accessKey, String secretAccessKey, long lifespan, Map content) throws Exception { 48 | return createToken(TokenPrefix.TASK.getValue(), accessKey, secretAccessKey, lifespan, content); 49 | } 50 | 51 | private static String createToken(String prefix, String accessKey, String secretAccessKey, long lifespan, Map content) throws Exception { 52 | LinkedHashMap map = new LinkedHashMap<>(); 53 | map.putAll(content); 54 | map.put("ak", accessKey); 55 | map.put("nonce", UUID.randomUUID().toString()); 56 | 57 | if (lifespan > 0) { 58 | map.put("expireAt", System.currentTimeMillis() + lifespan + ""); 59 | } 60 | 61 | String information = toJson(sortMap(map)); 62 | map.put("sig", createHmac(secretAccessKey, information)); 63 | 64 | String query = sortAndStringifyMap(map); 65 | 66 | 67 | return prefix + stringToBase64(query); 68 | } 69 | 70 | private static LinkedHashMap sortMap(Map object) { 71 | List keys = new ArrayList<>(object.keySet()); 72 | keys.sort(null); 73 | 74 | LinkedHashMap linkedHashMap = new LinkedHashMap<>(); 75 | for (int i = 0; i < keys.size(); i++) { 76 | linkedHashMap.put(keys.get(i), object.get(keys.get(i))); 77 | } 78 | return linkedHashMap; 79 | } 80 | 81 | /** 82 | * 因程序本身的 map 只有一层,而非嵌套 map,所以此方法没有实现嵌套 map 转 string 83 | * 可自行替换为其他 json stringify 实现 84 | */ 85 | private static String toJson(LinkedHashMap map) { 86 | Iterator> iterator= map.entrySet().iterator(); 87 | 88 | List result = new ArrayList<>(); 89 | while(iterator.hasNext()) { 90 | Map.Entry entry = iterator.next(); 91 | String value; 92 | if (entry.getValue() == null) { 93 | value = "null"; 94 | } else { 95 | value = entry.getValue(); 96 | } 97 | result.add("\"" + entry.getKey() + "\"" + ":" + "\"" + value + "\""); 98 | } 99 | return "{" + String.join(",", result) + "}"; 100 | } 101 | 102 | private static String createHmac(String key, String data) throws Exception { 103 | Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); 104 | SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"); 105 | sha256_HMAC.init(secret_key); 106 | 107 | return byteArrayToHexString(sha256_HMAC.doFinal(data.getBytes("UTF-8"))); 108 | } 109 | 110 | private static String sortAndStringifyMap(Map object) { 111 | List keys = new ArrayList<>(object.keySet()); 112 | keys.sort(null); 113 | 114 | List kvStrings = new ArrayList<>(); 115 | for (int i = 0; i < keys.size(); i++) { 116 | if (object.get(keys.get(i)) == null) { 117 | continue; 118 | } else { 119 | kvStrings.add(encodeURIComponent(keys.get(i)) + "=" + encodeURIComponent(object.get(keys.get(i)))); 120 | } 121 | } 122 | return String.join("&", kvStrings); 123 | } 124 | 125 | private static String stringToBase64(String str) throws UnsupportedEncodingException { 126 | return Base64.getEncoder().encodeToString(str.getBytes("utf-8")).replace("+", "-").replace("/", "_").replaceAll("=+$", ""); 127 | } 128 | 129 | private static String byteArrayToHexString(byte[] b) { 130 | StringBuilder hs = new StringBuilder(); 131 | String stmp; 132 | for (int n = 0; b!=null && n < b.length; n++) { 133 | stmp = Integer.toHexString(b[n] & 0XFF); 134 | if (stmp.length() == 1) 135 | hs.append('0'); 136 | hs.append(stmp); 137 | } 138 | return hs.toString().toLowerCase(); 139 | } 140 | 141 | /** 142 | * encodeURIComponent 基于 url 编码对字符串进行编码 143 | * 最终实现和 JavaScript 中的 encodeURIComponent 一致 144 | * 145 | * https://stackoverflow.com/questions/607176/java-equivalent-to-javascripts-encodeuricomponent-that-produces-identical-outpu 146 | */ 147 | private static String encodeURIComponent(String s) { 148 | String result = null; 149 | 150 | try { 151 | result = URLEncoder.encode(s, "UTF-8") 152 | .replaceAll("\\+", "%20") 153 | .replaceAll("%21", "!") 154 | .replaceAll("%27", "'") 155 | .replaceAll("%28", "(") 156 | .replaceAll("%29", ")") 157 | .replaceAll("%7E", "~"); 158 | } 159 | // This exception should never occur. 160 | catch (UnsupportedEncodingException e) { 161 | result = s; 162 | } 163 | 164 | return result; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Agora 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 | -------------------------------------------------------------------------------- /Node/JavaScript/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | 15 | [*.{yaml, yml}] 16 | indent_size = 2 17 | 18 | [*.js] 19 | quote_type = double 20 | -------------------------------------------------------------------------------- /Node/JavaScript/README.md: -------------------------------------------------------------------------------- 1 | ## JavaScript 2 | 3 | > 需要注意的是,此实现并没有发到 `npm` 上,需使用方自行复制 4 | 5 | ### 使用实例 6 | 7 | #### SDK Token 8 | 9 | ```javascript 10 | const { sdkToken, TokenPrefix } = require("./index"); 11 | 12 | // 生成 sdk token 13 | const netlessSDKToken = sdkToken( 14 | "netless ak", 15 | "netless sk", 16 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 17 | { 18 | role: TokenRole.Admin // 可以选择 TokenRole.Reader / TokenRole.Writer 19 | } 20 | ); 21 | ``` 22 | 23 | #### Room Token 24 | 25 | ```javascript 26 | const { roomToken, TokenPrefix } = require("./index"); 27 | 28 | // 生成 room token 29 | const netlessRoomToken = roomToken( 30 | "netless ak", 31 | "netless sk", 32 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 33 | { 34 | role: TokenRole.Reader, // 可以选择 TokenRole.Admin / TokenRole.Writer 35 | uuid: "房间的 UUID" 36 | } 37 | ); 38 | ``` 39 | 40 | #### Task Token 41 | 42 | ```javascript 43 | const { taskToken, TokenPrefix } = require("./index"); 44 | 45 | 46 | // 生成 task token 47 | const netlessTaskToken = taskToken( 48 | "netless ak", 49 | "netless sk", 50 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 51 | { 52 | role: TokenRole.Writer, // 可以选择 TokenRole.Admin / TokenRole.Reader 53 | uuid: "任务的 UUID" 54 | } 55 | ); 56 | ``` 57 | -------------------------------------------------------------------------------- /Node/JavaScript/browser.js: -------------------------------------------------------------------------------- 1 | // you should also add this line: 2 | // 3 | // which imports `uuidv1()` function 4 | 5 | /** 6 | * Usage: 7 | * ```js 8 | * await NetlessToken.sdkToken("ak", "sk", 60 * 1000, 9 | * { role: NetlessToken.TokenRole.Admin }); 10 | * await NetlessToken.roomToken("ak", "sk", 60 * 1000, 11 | * { role: NetlessToken.TokenRole.Writer, uuid: }); 12 | * await NetlessToken.taskToken("ak", "sk", 60 * 1000, 13 | * { role: NetlessToken.TokenRole.Reader, uuid: }); 14 | * ``` 15 | */ 16 | var NetlessToken = (function (exports, uuidv1) { 17 | const TokenRole = { 18 | Admin: "0", 19 | Writer: "1", 20 | Reader: "2", 21 | }; 22 | exports.TokenRole = TokenRole; 23 | 24 | const TokenPrefix = { 25 | SDK: "NETLESSSDK_", 26 | ROOM: "NETLESSROOM_", 27 | TASK: "NETLESSTASK_", 28 | }; 29 | exports.TokenPrefix = TokenPrefix; 30 | 31 | function formatJSON(obj) { 32 | const keys = Object.keys(obj).sort(); 33 | const target = {}; 34 | 35 | for (const key of keys) { 36 | target[key] = String(obj[key]); 37 | } 38 | 39 | return target; 40 | } 41 | 42 | function stringify(obj) { 43 | return Object.keys(obj) 44 | .map((key) => { 45 | const value = obj[key]; 46 | if (value === undefined) return ""; 47 | if (value === null) return "null"; 48 | return `${encodeURIComponent(key)}=${encodeURIComponent( 49 | value 50 | )}`; 51 | }) 52 | .join("&"); 53 | } 54 | 55 | const encoder = new TextEncoder("utf-8"); 56 | 57 | async function createHmac(hash, secretKey) { 58 | if (hash === "sha256") hash = "SHA-256"; 59 | const key = await crypto.subtle.importKey( 60 | "raw", 61 | encoder.encode(secretKey), 62 | { 63 | name: "HMAC", 64 | hash: hash, 65 | }, 66 | true, 67 | ["sign", "verify"] 68 | ); 69 | return { 70 | data: "", 71 | update(str) { 72 | this.data += str; 73 | return this; 74 | }, 75 | async digest() { 76 | const data = encoder.encode(this.data); 77 | const sig = await crypto.subtle.sign("HMAC", key, data); 78 | const b = Array.from(new Uint8Array(sig)); 79 | return b.map((x) => x.toString(16).padStart(2, "0")).join(""); 80 | }, 81 | }; 82 | } 83 | 84 | function createToken(prefix) { 85 | return async (accessKey, secretAccessKey, lifespan, content) => { 86 | const object = { 87 | ...content, 88 | ak: accessKey, 89 | nonce: uuidv1(), 90 | }; 91 | 92 | if (lifespan > 0) { 93 | object.expireAt = `${Date.now() + lifespan}`; 94 | } 95 | 96 | const information = JSON.stringify(formatJSON(object)); 97 | const hmac = await createHmac("sha256", secretAccessKey); 98 | object.sig = await hmac.update(information).digest("hex"); 99 | const query = stringify(formatJSON(object)); 100 | 101 | return ( 102 | prefix + 103 | btoa(query) 104 | .replace(/\+/g, "-") 105 | .replace(/\//g, "_") 106 | .replace(/=+$/, "") 107 | ); 108 | }; 109 | } 110 | 111 | exports.sdkToken = createToken(TokenPrefix.SDK); 112 | exports.roomToken = createToken(TokenPrefix.ROOM); 113 | exports.taskToken = createToken(TokenPrefix.TASK); 114 | 115 | return exports; 116 | })({}, uuidv1); 117 | -------------------------------------------------------------------------------- /Node/JavaScript/index.js: -------------------------------------------------------------------------------- 1 | const { createHmac } = require("crypto"); 2 | const { v1: uuidv1 } = require("uuid"); 3 | 4 | 5 | // 数字越小,权限越大 6 | const TokenRole = { 7 | Admin: "0", 8 | Writer: "1", 9 | Reader: "2", 10 | }; 11 | module.exports.TokenRole = TokenRole; 12 | 13 | const TokenPrefix = { 14 | SDK: "NETLESSSDK_", 15 | ROOM: "NETLESSROOM_", 16 | TASK: "NETLESSTASK_", 17 | }; 18 | module.exports.TokenPrefix = TokenPrefix 19 | 20 | /** 21 | * buffer 转 base64,且格式化字符 22 | * @param {Buffer} buffer - 要格式化的 buffer 对象 23 | * @return {string} 24 | */ 25 | const bufferToBase64 = buffer => { 26 | return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); 27 | }; 28 | 29 | /** 30 | * 为对象排序,并把非字符串的 value 转成 string 31 | * @param {Object} object - 要转换的对象 32 | * @return {Object} 33 | */ 34 | const formatJSON = object => { 35 | const keys = Object.keys(object).sort(); 36 | const target = {}; 37 | 38 | for (const key of keys) { 39 | target[key] = String(object[key]); 40 | } 41 | return target; 42 | }; 43 | 44 | /** 45 | * 序列化对象 46 | * 把 undefined 转为空字符串 47 | * 把 null 转为 "null" 48 | * 最终通过 & 连接成一个字符串 49 | * 实现强参考: https://github.com/sindresorhus/query-string/blob/master/index.js#L284 50 | * @param {Object} object - 将要序列化的对象 51 | * @return {string} 52 | */ 53 | const stringify = (object) => { 54 | return Object.keys(object) 55 | .map(key => { 56 | const value = object[key]; 57 | 58 | if (value === undefined) { 59 | return ""; 60 | } 61 | 62 | if (value === null) { 63 | return "null"; 64 | } 65 | 66 | return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; 67 | }) 68 | .join("&"); 69 | }; 70 | 71 | /** 72 | * sdk token 所需额外参数 73 | * @typedef {Object} SdkToken 74 | * @property {number} [role] - 权限等级,有 0~2,越小权限越大 75 | */ 76 | /** 77 | * room token 所需额外参数 78 | * @typedef {Object} RoomToken 79 | * @property {number} [role] - 权限等级,有 0~2,越小权限越大 80 | * @property {string} [uuid] - uuid 81 | */ 82 | /** 83 | * task token 所需额外参数 84 | * @typedef {Object} TaskToken 85 | * @property {number} [role] - 权限等级,有 0~2,越小权限越大 86 | * @property {string} [uuid] - uuid 87 | */ 88 | /** 89 | * 根据 prefix 生成相应的 token 90 | * @param {"NETLESSSDK_" | "NETLESSROOM_" | "NETLESSTASK_"} prefix - 相关 token 的前缀 91 | * @return {function(string, string, number, SdkToken | RoomToken | TaskToken): string} 92 | */ 93 | const createToken = prefix => { 94 | return (accessKey, secretAccessKey, lifespan, content) => { 95 | const object = { 96 | ...content, 97 | ak: accessKey, 98 | nonce: uuidv1(), 99 | }; 100 | 101 | if (lifespan > 0) { 102 | object.expireAt = `${Date.now() + lifespan}`; 103 | } 104 | 105 | const information = JSON.stringify(formatJSON(object)); 106 | const hmac = createHmac("sha256", secretAccessKey); 107 | object.sig = hmac.update(information).digest("hex"); 108 | 109 | const query = stringify(formatJSON(object)); 110 | const buffer = Buffer.from(query, "utf8"); 111 | 112 | return prefix + bufferToBase64(buffer); 113 | }; 114 | }; 115 | 116 | // 生成 sdk token 117 | module.exports.sdkToken = createToken(TokenPrefix.SDK); 118 | 119 | // 生成 room token 120 | module.exports.roomToken = createToken(TokenPrefix.ROOM); 121 | 122 | // 生成 task token 123 | module.exports.taskToken = createToken(TokenPrefix.TASK); 124 | -------------------------------------------------------------------------------- /Node/JavaScript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "node index.js" 7 | }, 8 | "author": "", 9 | "license": "MIT", 10 | "dependencies": { 11 | "uuid": "^8.3.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Node/JavaScript/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | uuid@^8.3.0: 6 | version "8.3.0" 7 | resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" 8 | integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== 9 | -------------------------------------------------------------------------------- /Node/TypeScript/.babelrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", { 6 | "modules": false 7 | } 8 | ], [ 9 | "@babel/preset-typescript" 10 | ] 11 | ], 12 | "plugins": [ 13 | "@babel/plugin-transform-runtime", 14 | "transform-class-properties" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /Node/TypeScript/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | 15 | [*.{yaml, yml}] 16 | indent_size = 2 17 | 18 | [*.ts] 19 | quote_type = double -------------------------------------------------------------------------------- /Node/TypeScript/.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | node_modules/ 3 | dist/ 4 | idea/ -------------------------------------------------------------------------------- /Node/TypeScript/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: [ 4 | 'prettier', 5 | '@typescript-eslint' 6 | ], 7 | parserOptions: { 8 | project: './tsconfig.json', 9 | }, 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:@typescript-eslint/recommended', 13 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 14 | 'prettier/@typescript-eslint', 15 | 'plugin:prettier/recommended', 16 | ], 17 | rules: { 18 | "@typescript-eslint/array-type": [ 19 | "error", 20 | { 21 | "default": "array" 22 | } 23 | ], 24 | "@typescript-eslint/consistent-type-assertions": "error", 25 | "@typescript-eslint/explicit-member-accessibility": [ 26 | "error", 27 | { 28 | "accessibility": "explicit", 29 | "overrides": { 30 | "accessors": "explicit", 31 | "constructors": "explicit" 32 | } 33 | } 34 | ], 35 | "@typescript-eslint/member-delimiter-style": [ 36 | "error", 37 | { 38 | "multiline": { 39 | "delimiter": "semi", 40 | "requireLast": true 41 | }, 42 | "singleline": { 43 | "delimiter": "semi", 44 | "requireLast": false 45 | } 46 | } 47 | ], 48 | "@typescript-eslint/no-unused-expressions": "error", 49 | "@typescript-eslint/prefer-namespace-keyword": "error", 50 | "@typescript-eslint/quotes": [ 51 | "error", 52 | "double" 53 | ], 54 | "@typescript-eslint/semi": [ 55 | "error", 56 | "always" 57 | ], 58 | "@typescript-eslint/type-annotation-spacing": "error", 59 | "arrow-parens": [ 60 | "error", 61 | "as-needed" 62 | ], 63 | "brace-style": [ 64 | "error", 65 | "1tbs" 66 | ], 67 | "comma-dangle": [ 68 | "error", 69 | "always-multiline" 70 | ], 71 | "curly": "error", 72 | "eqeqeq": [ 73 | "error", 74 | "always" 75 | ], 76 | "no-eval": "error", 77 | "no-invalid-this": "off", 78 | "no-throw-literal": "error", 79 | "no-trailing-spaces": "error", 80 | "no-unsafe-finally": "error", 81 | "prefer-const": "error", 82 | "spaced-comment": [ 83 | "error", 84 | "always", 85 | { 86 | "markers": [ 87 | "/" 88 | ] 89 | } 90 | ], 91 | "use-isnan": "error", 92 | 'no-var': 'error', // Disable var 93 | 'semi': [ // Always use semi 94 | 'error', 95 | 'always', 96 | ], 97 | '@typescript-eslint/explicit-module-boundary-types': 'off', 98 | '@typescript-eslint/ban-ts-comment': 'off', 99 | '@typescript-eslint/no-unsafe-call': 'off', 100 | '@typescript-eslint/no-unsafe-return': 'off', 101 | '@typescript-eslint/no-explicit-any': 'off', 102 | '@typescript-eslint/ban-types': 'off', 103 | '@typescript-eslint/unbound-method': 'off', 104 | '@typescript-eslint/no-unsafe-assignment': 'off', 105 | '@typescript-eslint/no-unsafe-member-access': 'off', 106 | 'prettier/prettier': 'error', 107 | '@typescript-eslint/restrict-template-expressions': 'off', 108 | '@typescript-eslint/no-misused-promises': 'off', 109 | }, 110 | } -------------------------------------------------------------------------------- /Node/TypeScript/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "printWidth": 100, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "arrowParens": "avoid", 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | } 10 | -------------------------------------------------------------------------------- /Node/TypeScript/README.md: -------------------------------------------------------------------------------- 1 | ## Typescript 2 | 3 | > 需要注意的是,此实现并没有发到 `npm` 上,需使用方自行复制 4 | 5 | ### 使用实例 6 | 7 | #### SDK Token 8 | 9 | ```typescript 10 | import { sdkToken, TokenPrefix } from "./src/index"; 11 | 12 | // 生成 sdk token 13 | const netlessSDKToken = sdkToken( 14 | "netless ak", 15 | "netless sk", 16 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 17 | { 18 | role: TokenRole.Admin // 可以选择 TokenRole.Reader / TokenRole.Writer 19 | } 20 | ); 21 | ``` 22 | 23 | #### Room Token 24 | 25 | ```typescript 26 | import { roomToken, TokenPrefix } from "./src/index"; 27 | 28 | // 生成 sdk token 29 | const netlessRoomToken = roomToken( 30 | "netless ak", 31 | "netless sk", 32 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 33 | { 34 | role: TokenRole.Reader, // 可以选择 TokenRole.Admin / TokenRole.Writer 35 | uuid: "房间的 UUID" 36 | } 37 | ); 38 | ``` 39 | 40 | #### Task Token 41 | 42 | ```typescript 43 | import { taskToken, TokenPrefix } from "./src/index"; 44 | 45 | // 生成 sdk token 46 | const netlessTaskToken = taskToken( 47 | "netless ak", 48 | "netless sk", 49 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 50 | { 51 | role: TokenRole.Writer, // 可以选择 TokenRole.Admin / TokenRole.Reader 52 | uuid: "任务的 UUID" 53 | } 54 | ); 55 | ``` 56 | -------------------------------------------------------------------------------- /Node/TypeScript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netless-token", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "webpack --watch", 8 | "build": "webpack --progress" 9 | }, 10 | "devDependencies": { 11 | "@babel/core": "^7.11.4", 12 | "@babel/plugin-transform-runtime": "^7.11.0", 13 | "@babel/preset-env": "^7.11.0", 14 | "@babel/preset-typescript": "^7.10.4", 15 | "@types/node": "^14.6.2", 16 | "@types/uuid": "^8.3.0", 17 | "@typescript-eslint/eslint-plugin": "^3.10.1", 18 | "@typescript-eslint/parser": "^3.10.1", 19 | "babel-loader": "^8.1.0", 20 | "babel-plugin-transform-class-properties": "^6.24.1", 21 | "clean-webpack-plugin": "^3.0.0", 22 | "eslint": "^7.7.0", 23 | "eslint-config-prettier": "^6.11.0", 24 | "eslint-loader": "^4.0.2", 25 | "eslint-plugin-prettier": "^3.1.4", 26 | "fork-ts-checker-webpack-plugin": "^5.1.0", 27 | "prettier": "^2.1.1", 28 | "typescript": "^4.0.2", 29 | "webpack": "^4.44.1", 30 | "webpack-cli": "^3.3.12", 31 | "webpack-merge": "^5.1.3", 32 | "webpack-node-externals": "^2.5.2" 33 | }, 34 | "dependencies": { 35 | "uuid": "^8.3.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Node/TypeScript/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createHmac } from "crypto"; 2 | import { v1 as uuidv1 } from "uuid"; 3 | 4 | export enum TokenRole { 5 | // 数字越小,权限越大 6 | Admin = "0", 7 | Writer = "1", 8 | Reader = "2", 9 | } 10 | 11 | export enum TokenPrefix { 12 | SDK = "NETLESSSDK_", 13 | ROOM = "NETLESSROOM_", 14 | TASK = "NETLESSTASK_", 15 | } 16 | 17 | // buffer 转 base64,且格式化字符 18 | const bufferToBase64 = (buffer: Buffer): string => { 19 | return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); 20 | }; 21 | 22 | // 排序,以确保最终生成的 string 与顺序无关 23 | // keys 的顺序不应该影响 hash 的值 24 | const formatJSON = (object: T): StrByObj => { 25 | const keys = Object.keys(object).sort(); 26 | const target: StrByObj = {}; 27 | 28 | for (const key of keys) { 29 | target[key] = String(object[key]); 30 | } 31 | return target; 32 | }; 33 | 34 | // 序列化对象 35 | const stringify = (object: StrByObj): string => { 36 | return Object.keys(object) 37 | .map(key => { 38 | const value = object[key]; 39 | 40 | if (value === undefined) { 41 | return ""; 42 | } 43 | 44 | if (value === null) { 45 | return "null"; 46 | } 47 | 48 | return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; 49 | }) 50 | .join("&"); 51 | }; 52 | 53 | // 根据相关 prefix 生成相应的token 54 | const createToken = ( 55 | prefix: TokenPrefix, 56 | ): ((accessKey: string, secretAccessKey: string, lifespan: number, content: T) => string) => { 57 | return (accessKey: string, secretAccessKey: string, lifespan: number, content: T) => { 58 | const object: StrAndIntByObj = { 59 | ...content, 60 | ak: accessKey, 61 | nonce: uuidv1(), 62 | }; 63 | 64 | if (lifespan > 0) { 65 | object.expireAt = `${Date.now() + lifespan}`; 66 | } 67 | 68 | const information = JSON.stringify(formatJSON(object)); 69 | const hmac = createHmac("sha256", secretAccessKey); 70 | object.sig = hmac.update(information).digest("hex"); 71 | 72 | const query = stringify(formatJSON(object)); 73 | const buffer = Buffer.from(query, "utf8"); 74 | 75 | return prefix + bufferToBase64(buffer); 76 | }; 77 | }; 78 | 79 | // 生成 sdk token 80 | export const sdkToken = createToken(TokenPrefix.SDK); 81 | 82 | // 生成 room token 83 | export const roomToken = createToken(TokenPrefix.ROOM); 84 | 85 | // 生成 task token 86 | export const taskToken = createToken(TokenPrefix.TASK); 87 | 88 | export type SdkTokenTags = { 89 | readonly role?: TokenRole; 90 | }; 91 | 92 | export type RoomTokenTags = { 93 | readonly uuid?: string; 94 | readonly role?: TokenRole; 95 | }; 96 | 97 | export type TaskTokenTags = { 98 | readonly uuid?: string; 99 | readonly role?: TokenRole; 100 | }; 101 | 102 | type StrAndIntByObj = Record; 103 | type StrByObj = Record; 104 | -------------------------------------------------------------------------------- /Node/TypeScript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "lib": ["es2017"], 6 | "sourceMap": true, 7 | "moduleResolution": "node", 8 | "removeComments": true, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true, 11 | "strictFunctionTypes": true, 12 | "noImplicitThis": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "allowSyntheticDefaultImports": true, 18 | "esModuleInterop": true, 19 | "emitDecoratorMetadata": true, 20 | "experimentalDecorators": true, 21 | "resolveJsonModule": true, 22 | "strictBindCallApply": true, 23 | "skipLibCheck": true, 24 | "skipDefaultLibCheck": true, 25 | "strict": true 26 | }, 27 | "exclude": ["node_modules"], 28 | "include": ["src/**/*.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /Node/TypeScript/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const nodeExternals = require('webpack-node-externals'); 3 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 4 | const { NamedModulesPlugin } = require("webpack"); 5 | const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); 6 | 7 | module.exports = { 8 | entry: [path.resolve(__dirname, "src", "index.ts")], 9 | target: "node", 10 | mode: "production", 11 | devtool: "source-map", 12 | 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.ts?$/, 17 | use: [ 18 | { 19 | loader: "babel-loader", 20 | }, 21 | { 22 | loader: "eslint-loader", 23 | options: { 24 | fix: true, 25 | }, 26 | }, 27 | ], 28 | exclude: /node_modules/, 29 | }, 30 | ], 31 | }, 32 | 33 | plugins: [ 34 | new NamedModulesPlugin(), 35 | new CleanWebpackPlugin(), 36 | new ForkTsCheckerWebpackPlugin({ 37 | typescript: { 38 | configFile: path.resolve(__dirname, "tsconfig.json"), 39 | diagnosticOptions: { 40 | semantic: true, 41 | syntactic: true, 42 | declaration: true, 43 | }, 44 | }, 45 | }), 46 | ], 47 | 48 | externals: [nodeExternals()], 49 | 50 | resolve: { 51 | extensions: [".ts", ".js"], 52 | }, 53 | 54 | output: { 55 | filename: "index.js", 56 | path: path.resolve(__dirname, "dist"), 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | # netless-token 2 | 3 | [中文版](README.md) 4 | 5 | This project is used to check out the Token that can be recognized by the [Agora Interactive Whiteboard](https://docs.agora.io/en/whiteboard/product_whiteboard?platform=Android) service. For details, please refer to [Interactive Whiteboard Token Overview](https://docs.agora.io/en/whiteboard/whiteboard_token_overview?platform=RESTful). 6 | 7 | ## How to use 8 | 9 | You need to obtain ``AK`` and ``SK`` (please refer to [Get access keys](https://docs.agora.io/en/whiteboard/whiteboard_token_overview?platform=Android#get-access-keys) for obtaining methods. After that, select the sample codes in the repo according to your own language and migrate them to your own project. Finally, when needed, Pass in ``AK`` and ``SK`` and call the function to generate Token. 10 | 11 | - [JavaScript](/Node/JavaScript) 12 | - [TypeScript](/Node/TypeScript) 13 | - [C#](/csharp) 14 | - [Go](/golang) 15 | - [PHP](/php) 16 | - [Ruby](/ruby) 17 | - [Python](/python) 18 | 19 | If the repo does not provide sample codes in a language that meets your needs, you can do the following. 20 | 21 | - Try to imitate the equivalent codes of the language you need based on the existing language. 22 | - To apply for a token by initiating an HTTP request to the Agora Interactive Whiteboard service, refer to [Generate a Token Using RESTful API](https://docs.agora.io/en/whiteboard/generate_whiteboard_token?platform=RESTful). But we do **NOT** recommend this approach. 23 | 24 | ## Note 25 | 26 | - ``AK`` and ``SK`` are important assets of your company or team. Do not transmit it to the client, or directly code it to the client. Getting ``AK`` and ``SK`` means getting everything, allowing malicious people to steal and seriously damage your asset security. 27 | - Tokens that never expire may bring security risks to your business. Imagine that if someone acquires a highly authorized token, he can use the token to harm your system, and the only way you can invalidate the token is to disable the access key pair of the token—this is a side effect Great operation. 28 | - Don't leak the sdkToken to the client (or front-end), and don't store the sdkToken in the database or write it into the configuration file. It should be checked out temporarily while in use, and the expiration time should be set as short as possible. The permission level of sdkToken is very high, and the leakage will endanger business security. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netless-token 2 | 3 | [English](README-en.md) 4 | 5 | 该项目用于签出 [Agora 互动白板服务](https://docs.agora.io/cn/whiteboard/product_whiteboard?platform=Android)可识别的 Token,具体请参考[《互动白板 Token 概述》](https://docs.agora.io/cn/whiteboard/product_whiteboard?platform=Android)。 6 | 7 | ## 如何使用 8 | 9 | 你需要获取 ``AK``、和 ``SK``(可参考[获取访问密钥对](https://docs.agora.io/cn/whiteboard/whiteboard_token_overview?platform=Android#获取访问密钥对)。之后,根据自己所掌握的语言,选择该 repo 中的 sample codes,将其迁移到自己的项目中。最后,在需要时,传入 ``AK`` 和 ``SK`` 并调用函数生成 Token。 10 | 11 | - [JavaScript](/Node/JavaScript) 12 | - [TypeScript](/Node/TypeScript) 13 | - [C#](/csharp) 14 | - [Go](/golang) 15 | - [PHP](/php) 16 | - [Ruby](/ruby) 17 | - [Python](/python) 18 | 19 | 如果该 repo 没有提供满足你需求的语言的 sample codes,你可以。 20 | 21 | - 尝试根据已有的语言仿写你需要语言的等效 codes。 22 | - 通过向 Agora 互动白板服务发起 HTTP 请求来申请 Token,参考[《生成 Token》](https://docs.agora.io/cn/whiteboard/generate_whiteboard_token?platform=RESTful)。但我们**不推荐**这种做法。 23 | 24 | ## 注意事项 25 | 26 | - ``AK``、``SK`` 是你的公司或团队的重要资产。切勿将其传输给客户端,或直接用代码写死在客户端。拿到了 ``AK``、``SK`` 就是拿到了一切,让恶意人士窃取严重破坏你的资产安全。 27 | - 永不过期的 Token 可能为你的业务带来安全隐患。想象一下,如果某人获取了一个权限很高的 Token,他就可以用该 Token 危害你的系统,而你将该 Token 失效的唯一手段只有禁用该 Token 的访问密钥对——这是一个副作用极大的操作。 28 | - 不要将 sdkToken 泄漏到客户端(或前端),也不要将 sdkToken 存入数据库或写入配置文件。应该在使用时临时签出,过期时间尽可能设短。sdkToken 的权限级别很高,泄漏后会危害业务安全。 29 | -------------------------------------------------------------------------------- /csharp/.gitignore: -------------------------------------------------------------------------------- 1 | # globs 2 | Makefile.in 3 | *.userprefs 4 | *.usertasks 5 | config.make 6 | config.status 7 | aclocal.m4 8 | install-sh 9 | autom4te.cache/ 10 | *.tar.gz 11 | tarballs/ 12 | test-results/ 13 | 14 | # Mac bundle stuff 15 | *.dmg 16 | *.app 17 | 18 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 19 | # General 20 | .DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | .com.apple.timemachine.donotpresent 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk 46 | 47 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 48 | # Windows thumbnail cache files 49 | Thumbs.db 50 | ehthumbs.db 51 | ehthumbs_vista.db 52 | 53 | # Dump file 54 | *.stackdump 55 | 56 | # Folder config file 57 | [Dd]esktop.ini 58 | 59 | # Recycle Bin used on file shares 60 | $RECYCLE.BIN/ 61 | 62 | # Windows Installer files 63 | *.cab 64 | *.msi 65 | *.msix 66 | *.msm 67 | *.msp 68 | 69 | # Windows shortcuts 70 | *.lnk 71 | 72 | # content below from: https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 73 | ## Ignore Visual Studio temporary files, build results, and 74 | ## files generated by popular Visual Studio add-ons. 75 | ## 76 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 77 | 78 | # User-specific files 79 | *.suo 80 | *.user 81 | *.userosscache 82 | *.sln.docstates 83 | 84 | # User-specific files (MonoDevelop/Xamarin Studio) 85 | *.userprefs 86 | 87 | # Build results 88 | [Dd]ebug/ 89 | [Dd]ebugPublic/ 90 | [Rr]elease/ 91 | [Rr]eleases/ 92 | x64/ 93 | x86/ 94 | bld/ 95 | [Bb]in/ 96 | [Oo]bj/ 97 | [Ll]og/ 98 | 99 | # Visual Studio 2015/2017 cache/options directory 100 | .vs/ 101 | # Uncomment if you have tasks that create the project's static files in wwwroot 102 | #wwwroot/ 103 | 104 | # Visual Studio 2017 auto generated files 105 | Generated\ Files/ 106 | 107 | # MSTest test Results 108 | [Tt]est[Rr]esult*/ 109 | [Bb]uild[Ll]og.* 110 | 111 | # NUNIT 112 | *.VisualState.xml 113 | TestResult.xml 114 | 115 | # Build Results of an ATL Project 116 | [Dd]ebugPS/ 117 | [Rr]eleasePS/ 118 | dlldata.c 119 | 120 | # Benchmark Results 121 | BenchmarkDotNet.Artifacts/ 122 | 123 | # .NET Core 124 | project.lock.json 125 | project.fragment.lock.json 126 | artifacts/ 127 | 128 | # StyleCop 129 | StyleCopReport.xml 130 | 131 | # Files built by Visual Studio 132 | *_i.c 133 | *_p.c 134 | *_h.h 135 | *.ilk 136 | *.meta 137 | *.obj 138 | *.iobj 139 | *.pch 140 | *.pdb 141 | *.ipdb 142 | *.pgc 143 | *.pgd 144 | *.rsp 145 | *.sbr 146 | *.tlb 147 | *.tli 148 | *.tlh 149 | *.tmp 150 | *.tmp_proj 151 | *_wpftmp.csproj 152 | *.log 153 | *.vspscc 154 | *.vssscc 155 | .builds 156 | *.pidb 157 | *.svclog 158 | *.scc 159 | 160 | # Chutzpah Test files 161 | _Chutzpah* 162 | 163 | # Visual C++ cache files 164 | ipch/ 165 | *.aps 166 | *.ncb 167 | *.opendb 168 | *.opensdf 169 | *.sdf 170 | *.cachefile 171 | *.VC.db 172 | *.VC.VC.opendb 173 | 174 | # Visual Studio profiler 175 | *.psess 176 | *.vsp 177 | *.vspx 178 | *.sap 179 | 180 | # Visual Studio Trace Files 181 | *.e2e 182 | 183 | # TFS 2012 Local Workspace 184 | $tf/ 185 | 186 | # Guidance Automation Toolkit 187 | *.gpState 188 | 189 | # ReSharper is a .NET coding add-in 190 | _ReSharper*/ 191 | *.[Rr]e[Ss]harper 192 | *.DotSettings.user 193 | 194 | # JustCode is a .NET coding add-in 195 | .JustCode 196 | 197 | # TeamCity is a build add-in 198 | _TeamCity* 199 | 200 | # DotCover is a Code Coverage Tool 201 | *.dotCover 202 | 203 | # AxoCover is a Code Coverage Tool 204 | .axoCover/* 205 | !.axoCover/settings.json 206 | 207 | # Visual Studio code coverage results 208 | *.coverage 209 | *.coveragexml 210 | 211 | # NCrunch 212 | _NCrunch_* 213 | .*crunch*.local.xml 214 | nCrunchTemp_* 215 | 216 | # MightyMoose 217 | *.mm.* 218 | AutoTest.Net/ 219 | 220 | # Web workbench (sass) 221 | .sass-cache/ 222 | 223 | # Installshield output folder 224 | [Ee]xpress/ 225 | 226 | # DocProject is a documentation generator add-in 227 | DocProject/buildhelp/ 228 | DocProject/Help/*.HxT 229 | DocProject/Help/*.HxC 230 | DocProject/Help/*.hhc 231 | DocProject/Help/*.hhk 232 | DocProject/Help/*.hhp 233 | DocProject/Help/Html2 234 | DocProject/Help/html 235 | 236 | # Click-Once directory 237 | publish/ 238 | 239 | # Publish Web Output 240 | *.[Pp]ublish.xml 241 | *.azurePubxml 242 | # Note: Comment the next line if you want to checkin your web deploy settings, 243 | # but database connection strings (with potential passwords) will be unencrypted 244 | *.pubxml 245 | *.publishproj 246 | 247 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 248 | # checkin your Azure Web App publish settings, but sensitive information contained 249 | # in these scripts will be unencrypted 250 | PublishScripts/ 251 | 252 | # NuGet Packages 253 | *.nupkg 254 | # The packages folder can be ignored because of Package Restore 255 | **/[Pp]ackages/* 256 | # except build/, which is used as an MSBuild target. 257 | !**/[Pp]ackages/build/ 258 | # Uncomment if necessary however generally it will be regenerated when needed 259 | #!**/[Pp]ackages/repositories.config 260 | # NuGet v3's project.json files produces more ignorable files 261 | *.nuget.props 262 | *.nuget.targets 263 | 264 | # Microsoft Azure Build Output 265 | csx/ 266 | *.build.csdef 267 | 268 | # Microsoft Azure Emulator 269 | ecf/ 270 | rcf/ 271 | 272 | # Windows Store app package directories and files 273 | AppPackages/ 274 | BundleArtifacts/ 275 | Package.StoreAssociation.xml 276 | _pkginfo.txt 277 | *.appx 278 | 279 | # Visual Studio cache files 280 | # files ending in .cache can be ignored 281 | *.[Cc]ache 282 | # but keep track of directories ending in .cache 283 | !*.[Cc]ache/ 284 | 285 | # Others 286 | ClientBin/ 287 | ~$* 288 | *~ 289 | *.dbmdl 290 | *.dbproj.schemaview 291 | *.jfm 292 | *.pfx 293 | *.publishsettings 294 | orleans.codegen.cs 295 | 296 | # Including strong name files can present a security risk 297 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 298 | #*.snk 299 | 300 | # Since there are multiple workflows, uncomment next line to ignore bower_components 301 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 302 | #bower_components/ 303 | 304 | # RIA/Silverlight projects 305 | Generated_Code/ 306 | 307 | # Backup & report files from converting an old project file 308 | # to a newer Visual Studio version. Backup files are not needed, 309 | # because we have git ;-) 310 | _UpgradeReport_Files/ 311 | Backup*/ 312 | UpgradeLog*.XML 313 | UpgradeLog*.htm 314 | ServiceFabricBackup/ 315 | *.rptproj.bak 316 | 317 | # SQL Server files 318 | *.mdf 319 | *.ldf 320 | *.ndf 321 | 322 | # Business Intelligence projects 323 | *.rdl.data 324 | *.bim.layout 325 | *.bim_*.settings 326 | *.rptproj.rsuser 327 | 328 | # Microsoft Fakes 329 | FakesAssemblies/ 330 | 331 | # GhostDoc plugin setting file 332 | *.GhostDoc.xml 333 | 334 | # Node.js Tools for Visual Studio 335 | .ntvs_analysis.dat 336 | node_modules/ 337 | 338 | # Visual Studio 6 build log 339 | *.plg 340 | 341 | # Visual Studio 6 workspace options file 342 | *.opt 343 | 344 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 345 | *.vbw 346 | 347 | # Visual Studio LightSwitch build output 348 | **/*.HTMLClient/GeneratedArtifacts 349 | **/*.DesktopClient/GeneratedArtifacts 350 | **/*.DesktopClient/ModelManifest.xml 351 | **/*.Server/GeneratedArtifacts 352 | **/*.Server/ModelManifest.xml 353 | _Pvt_Extensions 354 | 355 | # Paket dependency manager 356 | .paket/paket.exe 357 | paket-files/ 358 | 359 | # FAKE - F# Make 360 | .fake/ 361 | 362 | # JetBrains Rider 363 | .idea/ 364 | *.sln.iml 365 | 366 | # CodeRush personal settings 367 | .cr/personal 368 | 369 | # Python Tools for Visual Studio (PTVS) 370 | __pycache__/ 371 | *.pyc 372 | 373 | # Cake - Uncomment if you are using it 374 | # tools/** 375 | # !tools/packages.config 376 | 377 | # Tabs Studio 378 | *.tss 379 | 380 | # Telerik's JustMock configuration file 381 | *.jmconfig 382 | 383 | # BizTalk build output 384 | *.btp.cs 385 | *.btm.cs 386 | *.odx.cs 387 | *.xsd.cs 388 | 389 | # OpenCover UI analysis results 390 | OpenCover/ 391 | 392 | # Azure Stream Analytics local run output 393 | ASALocalRun/ 394 | 395 | # MSBuild Binary and Structured Log 396 | *.binlog 397 | 398 | # NVidia Nsight GPU debugger configuration file 399 | *.nvuser 400 | 401 | # MFractors (Xamarin productivity tool) working folder 402 | .mfractor/ 403 | 404 | # Local History for Visual Studio 405 | .localhistory/ -------------------------------------------------------------------------------- /csharp/Netless/Token.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | using Newtonsoft.Json; 6 | 7 | namespace Netless 8 | { 9 | public static class TokenRole 10 | { 11 | public static readonly string Admin = "0"; 12 | public static readonly string Writter = "1"; 13 | public static readonly string Reader = "2"; 14 | } 15 | 16 | public static class TokenPrefix 17 | { 18 | public static readonly string SDK = "NETLESSSDK_"; 19 | public static readonly string ROOM = "NETLESSROOM_"; 20 | public static readonly string TASK = "NETLESSTASK_"; 21 | } 22 | 23 | public class SdkContent 24 | { 25 | public string role; 26 | 27 | public SdkContent(string role) 28 | { 29 | this.role = role; 30 | } 31 | } 32 | 33 | public class RoomContent 34 | { 35 | public string role; 36 | public string uuid; 37 | 38 | public RoomContent(string role, string uuid) 39 | { 40 | this.role = role; 41 | this.uuid = uuid; 42 | } 43 | } 44 | 45 | public class TaskContent 46 | { 47 | public string role; 48 | public string uuid; 49 | 50 | public TaskContent(string role, string uuid) 51 | { 52 | this.role = role; 53 | this.uuid = uuid; 54 | } 55 | } 56 | 57 | public class NetlessToken 58 | { 59 | private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 60 | 61 | private static string CreateToken(string prefix, string accessKey, string secretAccessKey, long lifespan, IDictionary content) 62 | { 63 | IDictionary m = new SortedDictionary 64 | { 65 | { "ak", accessKey }, 66 | { "nonce", Guid.NewGuid().ToString() } 67 | }; 68 | MergeDictionary(content, m); 69 | 70 | if (lifespan > 0) 71 | { 72 | m.Add("expireAt", Convert.ToString(Convert.ToInt64((DateTime.Now - UnixEpoch).TotalMilliseconds + lifespan))); 73 | } 74 | 75 | string infomation = JsonConvert.SerializeObject(m); 76 | using (HMACSHA256 digest = new HMACSHA256(Encoding.ASCII.GetBytes(secretAccessKey))) 77 | { 78 | byte[] hmac = digest.ComputeHash(Encoding.ASCII.GetBytes(infomation)); 79 | m.Add("sig", ByteArrayToString(hmac)); 80 | } 81 | return prefix + ToBase64(Querify(m)); 82 | } 83 | 84 | private static string ToBase64(string m) 85 | { 86 | return Convert.ToBase64String(Encoding.ASCII.GetBytes(m)).TrimEnd('=').Replace('+', '-').Replace('/', '_'); 87 | } 88 | 89 | private static string ByteArrayToString(byte[] a) 90 | { 91 | StringBuilder hex = new StringBuilder(a.Length * 2); 92 | foreach (byte b in a) 93 | { 94 | hex.AppendFormat("{0:x2}", b); 95 | } 96 | return hex.ToString(); 97 | } 98 | 99 | private static void MergeDictionary(IDictionary from, IDictionary to) 100 | { 101 | foreach (var entry in from) 102 | { 103 | to.Add(entry.Key, entry.Value); 104 | } 105 | } 106 | 107 | private static string Querify(IDictionary a) 108 | { 109 | List parts = new List(); 110 | foreach (var e in a) 111 | { 112 | parts.Add($"{Uri.EscapeDataString(e.Key)}={Uri.EscapeDataString(e.Value)}"); 113 | } 114 | return string.Join('&', parts); 115 | } 116 | 117 | public static string SdkToken(string accessKey, string secretAccessKey, long lifespan, SdkContent content) 118 | { 119 | IDictionary m = new SortedDictionary 120 | { 121 | { "role", content.role } 122 | }; 123 | return CreateToken(TokenPrefix.SDK, accessKey, secretAccessKey, lifespan, m); 124 | } 125 | 126 | public static string RoomToken(string accessKey, string secretAccessKey, long lifespan, RoomContent content) 127 | { 128 | IDictionary m = new SortedDictionary 129 | { 130 | { "role", content.role }, 131 | { "uuid", content.uuid } 132 | }; 133 | return CreateToken(TokenPrefix.ROOM, accessKey, secretAccessKey, lifespan, m); 134 | } 135 | 136 | public static string TaskToken(string accessKey, string secretAccessKey, long lifespan, TaskContent content) 137 | { 138 | IDictionary m = new SortedDictionary 139 | { 140 | { "role", content.role }, 141 | { "uuid", content.uuid } 142 | }; 143 | return CreateToken(TokenPrefix.TASK, accessKey, secretAccessKey, lifespan, m); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /csharp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Netless; 3 | 4 | class Program 5 | { 6 | static void Main(string[] args) 7 | { 8 | string token = NetlessToken.SdkToken("ak", "sk", 1000 * 60 * 10, new SdkContent(TokenRole.Admin)); 9 | 10 | Console.WriteLine(token); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /csharp/README.md: -------------------------------------------------------------------------------- 1 | ## C\# 2 | 3 | > 需要注意的是,此实现并没有发到 `nuget` 上,需使用方自行复制 4 | 5 | ### 使用实例 6 | 7 | #### SDK Token 8 | 9 | ```csharp 10 | using Netless; 11 | 12 | class Program 13 | { 14 | static void Main(string[] args) 15 | { 16 | string token = NetlessToken.SdkToken( 17 | "netless ak", 18 | "netless sk", 19 | 1000 * 60 * 10, // token 有效时间 (10 分钟), 为 0 时, 即永不过期. 单位毫秒 20 | new SdkContent( 21 | TokenRole.Admin // 可以选择 Admin/Writter/Reader 22 | ) 23 | ); 24 | } 25 | } 26 | ``` 27 | 28 | #### Room Token 29 | 30 | ```csharp 31 | using Netless; 32 | 33 | class Program 34 | { 35 | static void Main(string[] args) 36 | { 37 | string token = NetlessToken.RoomToken( 38 | "netless ak", 39 | "netless sk", 40 | 1000 * 60 * 10, // token 有效时间 (10 分钟), 为 0 时, 即永不过期. 单位毫秒 41 | new RoomContent( 42 | TokenRole.Admin, // 可以选择 Admin/Writter/Reader 43 | "房间的 UUID" 44 | ) 45 | ); 46 | } 47 | } 48 | ``` 49 | 50 | #### Task Token 51 | 52 | ```csharp 53 | using Netless; 54 | 55 | class Program 56 | { 57 | static void Main(string[] args) 58 | { 59 | string token = NetlessToken.TaskToken( 60 | "netless ak", 61 | "netless sk", 62 | 1000 * 60 * 10, // token 有效时间 (10 分钟), 为 0 时, 即永不过期. 单位毫秒 63 | new TaskContent( 64 | TokenRole.Admin, // 可以选择 Admin/Writter/Reader 65 | "任务的 UUID" 66 | ) 67 | ); 68 | } 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /csharp/csharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /csharp/csharp.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csharp", "csharp.csproj", "{5FE2F08F-C679-412F-BF8E-D43171E36ACC}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Any CPU = Debug|Any CPU 9 | Release|Any CPU = Release|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {5FE2F08F-C679-412F-BF8E-D43171E36ACC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {5FE2F08F-C679-412F-BF8E-D43171E36ACC}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {5FE2F08F-C679-412F-BF8E-D43171E36ACC}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {5FE2F08F-C679-412F-BF8E-D43171E36ACC}.Release|Any CPU.Build.0 = Release|Any CPU 16 | EndGlobalSection 17 | EndGlobal 18 | -------------------------------------------------------------------------------- /golang/README.md: -------------------------------------------------------------------------------- 1 | ## GoLang 2 | 3 | > 需要注意的是,此实现并没有打 `release/tag`,需使用方自行复制 4 | 5 | ### 使用实例 6 | 7 | #### SDK Token 8 | 9 | ```go 10 | import "your path" 11 | 12 | c := token.SDKContent{ 13 | role: token.AdminRole, // 可以选择 token.ReaderRole / token.WriterRole 14 | } 15 | 16 | netlessSDKToken := token.SDKToken( 17 | "netless ak", 18 | "netless sk", 19 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 20 | &c 21 | ) 22 | ``` 23 | 24 | #### Room Token 25 | 26 | ```go 27 | import "your path" 28 | 29 | c := token.RoomContent{ 30 | role: token.ReaderRole, // 可以选择 token.AdminRole / token.WriterRole 31 | uuid: "房间 UUID", 32 | } 33 | 34 | netlessRoomToken := token.RoomToken( 35 | "netless ak", 36 | "netless sk", 37 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 38 | &c 39 | ) 40 | ``` 41 | 42 | #### Task Token 43 | 44 | ```go 45 | import "your path" 46 | 47 | c := token.TaskContent{ 48 | role: token.WriterRole, // 可以选择 token.AdminRole / token.ReaderRole 49 | uuid: "任务 UUID", 50 | } 51 | 52 | netlessRoomToken := token.TaskToken( 53 | "netless ak", 54 | "netless sk", 55 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 56 | &c 57 | ) 58 | ``` 59 | -------------------------------------------------------------------------------- /golang/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/netless-io/netless-token/golang 2 | 3 | go 1.12 4 | 5 | require github.com/google/uuid v1.1.2 6 | -------------------------------------------------------------------------------- /golang/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 2 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | -------------------------------------------------------------------------------- /golang/token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/hex" 8 | "github.com/google/uuid" 9 | "net/url" 10 | "regexp" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | // 数字越小,权限越大 18 | const ( 19 | AdminRole = "0" 20 | WriterRole = "1" 21 | ReaderRole = "2" 22 | ) 23 | 24 | const ( 25 | sdkPrefix = "NETLESSSDK_" 26 | roomPrefix = "NETLESSROOM_" 27 | taskPrefix = "NETLESSTASK_" 28 | ) 29 | 30 | type SDKContent struct { 31 | Role string 32 | } 33 | 34 | type RoomContent struct { 35 | Role string 36 | Uuid string 37 | } 38 | 39 | type TaskContent struct { 40 | Role string 41 | Uuid string 42 | } 43 | 44 | // SDKToken 生成 sdk token 45 | func SDKToken(accessKey string, secretAccessKey string, lifespan int64, content *SDKContent) string { 46 | m := map[string]string{ 47 | "role": content.Role, 48 | } 49 | return createToken(sdkPrefix)(accessKey, secretAccessKey, lifespan, &m) 50 | } 51 | 52 | // RoomToken 生成 room token 53 | func RoomToken(accessKey string, secretAccessKey string, lifespan int64, content *RoomContent) string { 54 | m := map[string]string{ 55 | "role": content.Role, 56 | "uuid": content.Uuid, 57 | } 58 | return createToken(roomPrefix)(accessKey, secretAccessKey, lifespan, &m) 59 | } 60 | 61 | // TaskToken 生成 task token 62 | func TaskToken(accessKey string, secretAccessKey string, lifespan int64, content *TaskContent) string { 63 | m := map[string]string{ 64 | "role": content.Role, 65 | "uuid": content.Uuid, 66 | } 67 | return createToken(taskPrefix)(accessKey, secretAccessKey, lifespan, &m) 68 | } 69 | 70 | // bufferToBase64 buffer 转 base64 71 | // 并格式化字符 72 | func bufferToBase64(b []byte) string { 73 | str := base64.StdEncoding.EncodeToString(b) 74 | 75 | // 替换 "+" 到 "-" 76 | // 替换 "/" 到 "_" 77 | { 78 | r := strings.NewReplacer( 79 | "+", "-", 80 | "/", "_", 81 | ) 82 | str = r.Replace(str) 83 | } 84 | 85 | // 移除末尾所以的 "=" 86 | // 例如: hello== -> hello 87 | { 88 | r := regexp.MustCompile("=+$") 89 | str = r.ReplaceAllString(str, "") 90 | } 91 | 92 | return str 93 | } 94 | 95 | // getMapKeys 提取 map 里的 key 96 | // 并且以 key 为主进行排序 97 | func getMapKeys(m *map[string]string) []string { 98 | keys := make([]string, len(*m)) 99 | 100 | i := 0 101 | for k := range *m { 102 | keys[i] = k 103 | i++ 104 | } 105 | 106 | sort.Strings(keys) 107 | return keys 108 | } 109 | 110 | // encodeURIComponent 基于 url 编码对字符串进行编码 111 | // 最终实现和 JavaScript 中的 encodeURIComponent 一致 112 | func encodeURIComponent(str string) string { 113 | r := url.QueryEscape(str) 114 | 115 | // golang 的 url.QueryEscape 实现和 JavaScript 中的 encodeURIComponent 不一致的地方是对空格处理 116 | // golang 里会把空格转成 "+",所以这里通过字符串替换,再把 "+" 转成 "%20",以保证和 js 的实现一致 117 | r = strings.ReplaceAll(r, "+", "%20") 118 | return r 119 | } 120 | 121 | // stringify 序列化 Map 122 | // 实现逻辑可参考: https://github.com/sindresorhus/query-string/blob/master/index.js#L284 123 | func stringify(m *map[string]string) string { 124 | keys := getMapKeys(m) 125 | 126 | var arr []string 127 | for _, k := range keys { 128 | if (*m)[k] != "" { 129 | arr = append(arr, encodeURIComponent(k)+"="+encodeURIComponent((*m)[k])) 130 | } 131 | } 132 | 133 | return strings.Join(arr, "&") 134 | } 135 | 136 | // mergeMap 合并两个 map,返回新的 map 对象 137 | // 在有相同值的情况下,m2 的优先级更高 138 | func mergeMap(m1, m2 *map[string]string) map[string]string { 139 | result := make(map[string]string) 140 | 141 | for k, v := range *m1 { 142 | result[k] = v 143 | } 144 | 145 | for k, v := range *m2 { 146 | result[k] = v 147 | } 148 | 149 | return result 150 | } 151 | 152 | // jsonStringify 模范 JavaScript 中 JSON.stringify 的实现 153 | // 因程序本身的 map 只有一层,而非嵌套 map,所以此方法没有实现嵌套 map 转 string 154 | func jsonStringify(m *map[string]string) string { 155 | keys := getMapKeys(m) 156 | 157 | var body []string 158 | for _, k := range keys { 159 | body = append(body, "\""+k+"\""+":"+"\""+(*m)[k]+"\"") 160 | } 161 | 162 | result := strings.Join(body, ",") 163 | return "{" + result + "}" 164 | } 165 | 166 | // expireAt 根据当时时间及用户传入毫秒数,来计算过期时间 167 | func expireAt(lifespan int64) string { 168 | return strconv.FormatInt(time.Now().UTC().UnixNano()/1e6+lifespan, 10) 169 | } 170 | 171 | // hmac256 实现 hmac 加密并转成 hex,实现可参考 nodejs: crypto/createHmac 及 digest("hex") 172 | func hmac256(data string, secret string) string { 173 | key := []byte(secret) 174 | h := hmac.New(sha256.New, key) 175 | 176 | h.Write([]byte(data)) 177 | 178 | return hex.EncodeToString(h.Sum(nil)) 179 | } 180 | 181 | // createToken 根据 prefix 生成相应的 token 182 | func createToken(prefix string) func(string, string, int64, *map[string]string) string { 183 | return func(accessKey string, secretAccessKey string, lifespan int64, content *map[string]string) string { 184 | m := map[string]string{ 185 | "ak": accessKey, 186 | "nonce": uuid.Must(uuid.NewRandom()).String(), 187 | } 188 | m = mergeMap(content, &m) 189 | 190 | if lifespan > 0 { 191 | m["expireAt"] = expireAt(lifespan) 192 | } 193 | 194 | m["sig"] = hmac256(jsonStringify(&m), secretAccessKey) 195 | 196 | query := stringify(&m) 197 | return prefix + bufferToBase64([]byte(query)) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /golang/token_test.go: -------------------------------------------------------------------------------- 1 | package token_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func Test_bufferToBase64(t *testing.T) { 9 | b := bufferToBase64([]byte("netless")) 10 | 11 | if b != "bmV0bGVzcw" { 12 | t.Errorf("bufferToBase64([]byte(\"netless\")) expected be \"bmV0bGVzcw\", but %s got", b) 13 | } 14 | } 15 | 16 | func Test_getMapKeys(t *testing.T) { 17 | m := map[string]string{ 18 | "b": "1", 19 | "a": "2", 20 | } 21 | b := getMapKeys(&m) 22 | 23 | for i, k := range []string{"a", "b"} { 24 | if k != b[i] { 25 | t.Errorf("getMapKeys result expected be [a b], but %s got", b) 26 | } 27 | } 28 | } 29 | 30 | func Test_encodeURIComponent(t *testing.T) { 31 | s := encodeURIComponent("net less") 32 | 33 | if s != "net%20less" { 34 | t.Errorf("encodeURIComponent(\"net less\") expected be \"net%%20ess\", but %s got", s) 35 | } 36 | } 37 | 38 | func Test_stringify(t *testing.T) { 39 | m := map[string]string{ 40 | "b": "1", 41 | "a": "2", 42 | } 43 | s := stringify(&m) 44 | 45 | if s != "a=2&b=1" { 46 | t.Errorf("encodeURIComponent(\"net less\") expected be \"net%%20ess\", but %s got", s) 47 | } 48 | } 49 | 50 | func Test_mergeMap(t *testing.T) { 51 | m1 := map[string]string{ 52 | "b": "3", 53 | "c": "0", 54 | } 55 | 56 | m2 := map[string]string{ 57 | "a": "2", 58 | "b": "1", 59 | } 60 | s := mergeMap(&m1, &m2) 61 | 62 | if s["a"] != "2" || s["b"] != "1" || s["c"] != "0" { 63 | t.Errorf("mergeMap result expected be map[a:2 b:1 c:0], but %s got", s) 64 | } 65 | } 66 | 67 | func Test_jsonStringify(t *testing.T) { 68 | m := map[string]string{ 69 | "b": "1", 70 | "a": "2", 71 | } 72 | 73 | s := jsonStringify(&m) 74 | 75 | if s != "{\"a\":\"2\",\"b\":\"1\"}" { 76 | t.Errorf("jsonStringify result expected be \"{\"a\":\"2\",\"b\":\"1\"}\", but %s got", s) 77 | } 78 | } 79 | 80 | func Test_hmac256(t *testing.T) { 81 | s := hmac256("netless", "key") 82 | 83 | if s != "4b007294b9ebeccb96fb1ef30c843b9894d6323375e042b1ef2775978225ca7f" { 84 | t.Errorf("jsonStringify result expected be \"4b007294b9ebeccb96fb1ef30c843b9894d6323375e042b1ef2775978225ca7f\", but %s got", s) 85 | } 86 | } 87 | 88 | func Test_SDKToken(t *testing.T) { 89 | c := SDKContent{ 90 | Role: AdminRole, 91 | } 92 | s := SDKToken("netless", "x", 1, &c) 93 | 94 | if !strings.HasPrefix(s, sdkPrefix) { 95 | t.Errorf("SDKToken result expected prefix is %s, but result is: %s", sdkPrefix, s) 96 | } 97 | } 98 | 99 | func Test_RoomToken(t *testing.T) { 100 | c := RoomContent{ 101 | Role: ReaderRole, 102 | Uuid: "this is uuid", 103 | } 104 | s := RoomToken("netless", "x", 1, &c) 105 | 106 | if !strings.HasPrefix(s, roomPrefix) { 107 | t.Errorf("RoomToken result expected prefix is %s, but result is: %s", sdkPrefix, s) 108 | } 109 | } 110 | 111 | func Test_TaskToken(t *testing.T) { 112 | c := TaskContent{ 113 | Role: WriterRole, 114 | Uuid: "this is uuid", 115 | } 116 | s := TaskToken("netless", "x", 1, &c) 117 | 118 | if !strings.HasPrefix(s, taskPrefix) { 119 | t.Errorf("TaskToken result expected prefix is %s, but result is: %s", sdkPrefix, s) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /php/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /php/.php_cs: -------------------------------------------------------------------------------- 1 | exclude('vendor') 5 | ->in([__DIR__.'/src/', __DIR__.'/tests/']) 6 | ; 7 | 8 | return PhpCsFixer\Config::create() 9 | ->setRules([ 10 | '@PSR4' => true, 11 | ]) 12 | ->setFinder($finder) 13 | ; 14 | -------------------------------------------------------------------------------- /php/README.md: -------------------------------------------------------------------------------- 1 | ## PHP 2 | 3 | > 需要注意的是,此实现并没有发到 `packagist` 上,需使用方自行复制 4 | 5 | ### 使用实例 6 | 7 | #### SDK Token 8 | 9 | ```php 10 | use Netless\Token\Generate; 11 | 12 | $netlessToken = new Generate; 13 | $sdkToken = $netlessToken->sdkToken( 14 | "netless ak", 15 | "netless sk", 16 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 17 | array( 18 | "role" => Generate::AdminRole // 可以选择 Generate::ReaderRole / TokenRole.WriterRole 19 | ) 20 | ); 21 | ``` 22 | 23 | #### Room Token 24 | 25 | ```php 26 | use Netless\Token\Generate; 27 | 28 | $netlessToken = new Generate; 29 | $roomToken = $netlessToken->roomToken( 30 | "netless ak", 31 | "netless sk", 32 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 33 | array( 34 | "role" => Generate::ReaderRole, // 可以选择 Generate::AdminRole / TokenRole.WriterRole 35 | "uuid" => "房间的 UUID" 36 | ) 37 | ); 38 | ``` 39 | 40 | #### Task Token 41 | 42 | ```php 43 | use Netless\Token\Generate; 44 | 45 | $netlessToken = new Generate; 46 | $taskToken = $netlessToken->taskToken( 47 | "netless ak", 48 | "netless sk", 49 | 1000 * 60 * 10, // token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 50 | array( 51 | "role" => Generate::WriterRole // 可以选择 Generate::AdminRole / TokenRole.ReaderRole 52 | "uuid" => "任务的 UUID" 53 | ) 54 | ); 55 | ``` 56 | -------------------------------------------------------------------------------- /php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netless-token/php", 3 | "type": "library", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Black-Hole", 8 | "email": "158blackhole@gmail.com" 9 | } 10 | ], 11 | "scripts": { 12 | "phpcs": "php-cs-fixer fix --using-cache=no -vvv --diff --allow-risky=yes --show-progress=dots --config=.php_cs --ansi", 13 | "phpstan": "phpstan analyse --ansi", 14 | "test": "phpunit tests --colors=always --coverage-html=reports --whitelist src" 15 | }, 16 | "scripts-descriptions": { 17 | "phpcs": "Runs coding style test suite" 18 | }, 19 | "require-dev": { 20 | "friendsofphp/php-cs-fixer": "^2.16", 21 | "phpstan/phpstan": "^0.12.40", 22 | "phpstan/phpstan-strict-rules": "^0.12.5", 23 | "phpunit/phpunit": "^9" 24 | }, 25 | "description": "netless-token", 26 | "require": { 27 | "ramsey/uuid": "^4.1", 28 | "ext-json": "*" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Netless\\Token\\": "src/Netless/Token" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "classmap": [ 37 | "tests/" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /php/phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan-strict-rules/rules.neon 3 | 4 | parameters: 5 | level: 6 6 | paths: 7 | - src 8 | -------------------------------------------------------------------------------- /php/src/Netless/Token/Generate.php: -------------------------------------------------------------------------------- 1 | createToken("NETLESSSDK_")($accessKey, $secretAccessKey, $lifespan, $content); 27 | } 28 | 29 | /** 30 | * 生成 room token 31 | * @param string $accessKey netless ak 32 | * @param string $secretAccessKey netless sk 33 | * @param int $lifespan 过期时长,为 0 则永不过期 34 | * @param array $content 额外补充信息 35 | * @return string 36 | */ 37 | public function roomToken(string $accessKey, string $secretAccessKey, int $lifespan, array $content): string 38 | { 39 | return $this->createToken("NETLESSROOM_")($accessKey, $secretAccessKey, $lifespan, $content); 40 | } 41 | 42 | /** 43 | * 生成 task token 44 | * @param string $accessKey netless ak 45 | * @param string $secretAccessKey netless sk 46 | * @param int $lifespan 过期时长,为 0 则永不过期 47 | * @param array $content 额外补充信息 48 | * @return string 49 | */ 50 | public function taskToken(string $accessKey, string $secretAccessKey, int $lifespan, array $content): string 51 | { 52 | return $this->createToken("NETLESSTASK_")($accessKey, $secretAccessKey, $lifespan, $content); 53 | } 54 | 55 | /** 56 | * bufferToBase64 buffer 转 base64 57 | * 并格式化字符 58 | * @param string $str 需要转义的字符串 59 | * @return string 60 | */ 61 | private function bufferToBase64(string $str): string 62 | { 63 | $result = base64_encode($str); 64 | $result = preg_replace("/\+/", "-", $result, -1); 65 | $result = preg_replace("/\//", "_", $result, -1); 66 | return preg_replace("/=+$/", "", $result, -1); 67 | } 68 | 69 | /** 70 | * encodeURIComponent 基于 url 编码对字符串进行编码 71 | * 最终实现和 JavaScript 中的 encodeURIComponent 一致 72 | * @see https://stackoverflow.com/a/1734255/6596777 73 | * @param string $str 需要转换字符 74 | * @return string 75 | */ 76 | private function encodeURIComponent(string $str): string 77 | { 78 | $revert = array('%21'=>'!', '%2A'=>'*', '%27'=>"'", '%28'=>'(', '%29'=>')'); 79 | return strtr(rawurlencode($str), $revert); 80 | } 81 | 82 | /** 83 | * stringify 序列化 array 84 | * @param array $obj 需要转义的数组 85 | * @return string 86 | */ 87 | private function stringify(array $obj): string 88 | { 89 | $result = array(); 90 | foreach ($obj as $k => $v) { 91 | if ($v !== "") { 92 | array_push($result, $this->encodeURIComponent($k) . "=" . $this->encodeURIComponent($v)); 93 | } 94 | } 95 | return join("&", $result); 96 | } 97 | 98 | /** 99 | * 根据 prefix 生成相应的 generate 100 | * @param string $prefix 必须为: NETLESSSDK_ / NETLESSROOM_ / NETLESSTASK_ 101 | * @return \Closure 102 | */ 103 | private function createToken(string $prefix): \Closure 104 | { 105 | return function (string $accessKey, string $secretAccessKey, int $lifespan, array $content) use ($prefix): string { 106 | $map = array_replace($content, array( 107 | "ak" => $accessKey, 108 | "nonce" => Uuid::uuid4()->toString() 109 | )); 110 | 111 | if ($lifespan > 0) { 112 | $map += array( 113 | "expireAt" => strval((int)(microtime(true) * 1000) + $lifespan), 114 | ); 115 | } 116 | ksort($map); 117 | 118 | $map += array( 119 | "sig" => hash_hmac("sha256", json_encode($map), $secretAccessKey) 120 | ); 121 | ksort($map); 122 | 123 | return $prefix . $this->bufferToBase64($this->stringify($map)); 124 | }; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /php/tests/TokenTest.php: -------------------------------------------------------------------------------- 1 | sdkToken("a", "b", 1000 * 60 * 10, array( 12 | "role" => Generate::AdminRole 13 | )); 14 | 15 | $this->assertStringContainsString("NETLESSSDK_", $sdkToken); 16 | } 17 | 18 | public function testGenerateRoomToken() 19 | { 20 | $netlessToken = new Generate; 21 | $sdkToken = $netlessToken->roomToken("a", "b", 1000 * 60 * 10, array( 22 | "role" => Generate::ReaderRole, 23 | "uuid" => "this is uuid", 24 | )); 25 | 26 | $this->assertStringContainsString("NETLESSROOM_", $sdkToken); 27 | } 28 | 29 | public function testGenerateTaskToken() 30 | { 31 | $netlessToken = new Generate; 32 | $sdkToken = $netlessToken->taskToken("a", "b", 1000 * 60 * 10, array( 33 | "role" => Generate::WriterRole, 34 | "uuid" => "this is uuid", 35 | )); 36 | 37 | $this->assertStringContainsString("NETLESSTASK_", $sdkToken); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | ## Python 2 | 3 | > 需要注意的是,此实现并没有发到 `pip` 上,需使用方自行复制 4 | 5 | ### 使用实例 6 | 7 | #### SDK Token 8 | 9 | ```py 10 | import netless_token 11 | 12 | netless_token.sdk_token( 13 | "netless ak", 14 | "netless sk", 15 | 1000 * 60 * 10, # token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 16 | role=netless_token.ADMIN # 可以选择 netless_token.WRITER / netless_token.READER 17 | ) 18 | ``` 19 | 20 | #### Room Token 21 | 22 | ```py 23 | import netless_token 24 | 25 | netless_token.room_token( 26 | "netless ak", 27 | "netless sk", 28 | 1000 * 60 * 10, # token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 29 | role=netless_token.ADMIN, # 可以选择 netless_token.WRITER / netless_token.READER 30 | uuid="房间的 UUID" 31 | ) 32 | ``` 33 | 34 | #### Task Token 35 | 36 | ```py 37 | import netless_token 38 | 39 | netless_token.task_token( 40 | "netless ak", 41 | "netless sk", 42 | 1000 * 60 * 10, # token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 43 | role=netless_token.READER, # 可以选择 netless_token.WRITER / netless_token.ADMIN 44 | uuid="任务的 UUID" 45 | ) 46 | ``` 47 | -------------------------------------------------------------------------------- /python/netless_token.py: -------------------------------------------------------------------------------- 1 | # NOTE: one can still import "private" functions in this module 2 | # by `from netless_token import _create_token` or access `netless_token._create_token`, 3 | # the `__all__` only takes effect when `from netless_token import *` 4 | import urllib.parse 5 | import uuid 6 | import time 7 | import json 8 | import hmac 9 | import hashlib 10 | import base64 11 | 12 | __all__ = ["sdk_token", "room_token", "task_token", "ADMIN", "WRITER", "READER"] 13 | 14 | ADMIN = "0" 15 | WRITER = "1" 16 | READER = "2" 17 | 18 | 19 | def _create_token(prefix: str, access_key: str, secret_access_key: str, lifespan: int, content: dict) -> str: 20 | data = {"ak": access_key, "nonce": str(uuid.uuid1())} 21 | data.update(content) 22 | 23 | if lifespan > 0: 24 | data["expireAt"] = str(int(round(time.time() * 1000)) + lifespan) 25 | 26 | info = json.dumps(data, sort_keys=True, separators=(",", ":")) 27 | digest = hmac.new(bytes(secret_access_key, encoding="utf-8"), digestmod="SHA256") 28 | digest.update(bytes(info, encoding="utf-8")) 29 | data["sig"] = digest.hexdigest() 30 | encoded = base64.urlsafe_b64encode(bytes(urllib.parse.urlencode(data), encoding="utf-8")).decode("utf-8").rstrip("=") 31 | return prefix + encoded 32 | 33 | 34 | def sdk_token(access_key: str, secret_access_key: str, lifespan: int, role: str): 35 | return _create_token("NETLESSSDK_", access_key, secret_access_key, lifespan, content={'role': role}) 36 | 37 | 38 | def room_token(access_key: str, secret_access_key: str, lifespan: int, role: str, uuid: str): 39 | return _create_token("NETLESSROOM_", access_key, secret_access_key, lifespan, content={'role': role, 'uuid': uuid}) 40 | 41 | 42 | def task_token(access_key: str, secret_access_key: str, lifespan: int, role: str, uuid: str): 43 | return _create_token("NETLESSTASK_", access_key, secret_access_key, lifespan, content={'role': role, 'uuid': uuid}) 44 | -------------------------------------------------------------------------------- /ruby/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = false 8 | insert_final_newline = false 9 | 10 | [*.md] 11 | max_line_length = off 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'uuidtools', '~> 2.2' -------------------------------------------------------------------------------- /ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | uuidtools (2.2.0) 5 | 6 | PLATFORMS 7 | ruby 8 | 9 | DEPENDENCIES 10 | uuidtools (~> 2.2) 11 | 12 | BUNDLED WITH 13 | 2.1.4 14 | -------------------------------------------------------------------------------- /ruby/README.md: -------------------------------------------------------------------------------- 1 | ## Ruby 2 | 3 | > 需要注意的是,此实现并没有发到 `gem` 上,需使用方自行复制 4 | 5 | ### 使用实例 6 | 7 | #### SDK Token 8 | 9 | ```ruby 10 | require './lib/token.rb' 11 | 12 | NetlessToken.sdk_token( 13 | "netless ak", 14 | "netless sk", 15 | 1000 * 60 * 10, # token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 16 | { 17 | :role => NetlessToken::ROLE::ADMIN # 可以选择 NetlessToken::ROLE::WRITER / NetlessToken::ROLE::READER 18 | } 19 | ) 20 | ``` 21 | 22 | #### Room Token 23 | 24 | ```ruby 25 | require './lib/token.rb' 26 | 27 | NetlessToken.room_token( 28 | "netless ak", 29 | "netless sk", 30 | 1000 * 60 * 10, # token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 31 | { 32 | :role => NetlessToken::ROLE::READER # 可以选择 NetlessToken::ROLE::WRITER / NetlessToken::ROLE::ADMIN 33 | :uuid => "房间的 UUID" 34 | } 35 | ) 36 | ``` 37 | 38 | #### Task Token 39 | 40 | ```ruby 41 | require './lib/token.rb' 42 | 43 | NetlessToken.task_token( 44 | "netless ak", 45 | "netless sk", 46 | 1000 * 60 * 10, # token 有效时间 (10分钟),为 0 时,即永不过期。单位毫秒 47 | { 48 | :role => NetlessToken::ROLE::READER # 可以选择 NetlessToken::ROLE::WRITER / NetlessToken::ROLE::ADMIN 49 | :uuid => "任务的 UUID" 50 | } 51 | ) 52 | ``` -------------------------------------------------------------------------------- /ruby/lib/token.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "uuidtools" 3 | require "openssl" 4 | require "base64" 5 | 6 | module NetlessToken 7 | 8 | module ROLE 9 | ADMIN = "0" 10 | WRITER = "1" 11 | READER = "2" 12 | end 13 | 14 | module PREFIX 15 | SDK = "NETLESSSDK_" 16 | ROOM = "NETLESSROOM_" 17 | TASK = "NETLESSTASK_" 18 | end 19 | 20 | def self.to_base64(str) 21 | Base64.urlsafe_encode64(str, padding: false) 22 | end 23 | 24 | def self.create_token(prefix, access_key, secret_access_key, lifespan, content) 25 | object = { 26 | ak: access_key, 27 | nonce: UUIDTools::UUID.timestamp_create.to_s 28 | } 29 | object.merge!(content) 30 | 31 | if lifespan > 0 32 | object.store(:expireAt, ((Time.now.to_f * 1000).to_i + lifespan).to_s) 33 | end 34 | 35 | infomation = object.sort.to_h.to_json 36 | digest = OpenSSL::Digest.new('sha256') 37 | hmac = OpenSSL::HMAC.hexdigest(digest, secret_access_key, infomation) 38 | object.store(:sig, hmac) 39 | prefix + to_base64(URI.encode_www_form(object.sort.to_h)) 40 | end 41 | 42 | def self.sdk_token *args 43 | create_token PREFIX::SDK, *args 44 | end 45 | 46 | def self.room_token *args 47 | create_token PREFIX::ROOM, *args 48 | end 49 | 50 | def self.task_token *args 51 | create_token PREFIX::TASK, *args 52 | end 53 | end 54 | --------------------------------------------------------------------------------