diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,27 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "amd": true, + "node": true + }, + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "no-const-assign": "warn", + "no-this-before-super": "warn", + "no-undef": "error", + "no-unreachable": "warn", + "no-unused-vars": "warn", + "constructor-super": "warn", + "valid-typeof": "warn", + "semi" : "warn", + "no-invalid-this" : "error", + "no-console": "off" + } +} \ No newline at end of file diff --git a/.hgignore b/.hgignore new file mode 100644 --- /dev/null +++ b/.hgignore @@ -0,0 +1,5 @@ +syntax: glob +.gradle/ +build/ +node_modules/ +src/typings/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "java.configuration.updateBuildConfiguration": "disabled", + "tslint.enable": true, + "search.exclude": { + "**/node_modules": true, + "**/bower_components": true, + "/build": true + }, + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/node_modules/**": true, + "/build": true + }, + "editor.minimap.enabled": false +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 --- /dev/null +++ b/build.gradle @@ -0,0 +1,276 @@ +// если версия явно не заданы вычисляем ее из тэга ревизии v.{num}*** +// результатом будет версия '{num}.{distance}' где distance - расстояние от +// текущей ревизии до ревизии с тэгом +def tagDistance = 0; +def isRelease = false; + +if (!version) { + + def rev = ["hg", "log", "-r", ".", "--template", "{latesttag('re:^v') % '{tag}-{distance}'}"].execute().text.trim(); + + def tagVersion; + + def match = (rev =~ /^v(\d+\.\d+\.\d+).*-(\d+)$/); + + if (match.size()) { + tagVersion = match[0][1]; + tagDistance = match[0][2].toInteger(); + + version = tagVersion; + + if (tagDistance > 0) + version++; + } else { + throw new Exception("A version must be specied"); + } +} else { + println "explicit version: $version"; +} + +if (hasProperty('versionSuffix') && versionSuffix) { + version += "-$versionSuffix" +} + +if(!npmName) + npmName = name; + +if (hasProperty('release')) { + isRelease = (release != 'false') +} else { + isRelease = (tagDistance == 0); +} + +if(!["amd", "commonjs", "system", "umd", "es6", "esnext"].contains(jsmodule)) + throw new Exception("Invalid jsmodule specified: $jsmodule"); +if(!["es3", "es5", "es6", "es2016", "es2017", "esnext"].contains(target)) + throw new Exception("Invalid target specified: $target") + +def targetLibs = [ + "es3" : "es5,es2015.promise,es2015.symbol,dom,scripthost", + "es5" : "es5,es2015.promise,es2015.symbol,dom,scripthost" +]; + +ext.packageName="@$npmScope/$npmName"; + +def srcDir = "$projectDir/src" +def typingsDir = "$srcDir/typings" +def distDir = "$buildDir/dist" +def testDir = "$buildDir/test" +def lib = targetLibs[target] ?: "${target},dom"; + +println "lib: $lib"; + +def sourceSets = ["main", "amd", "cjs"]; +def testSets = ["test", "testAmd", "testCjs"]; + +task beforeBuild { +} + +def createSoursetTasks = { String name, String outDir -> + def setName = name.capitalize(); + + def compileDir = "$buildDir/compile/$name" + def declDir = "$typingsDir/$name" + def setDir = "$projectDir/src/$name" + def jsDir = outDir; + + def beforeBuildTask = task "beforeBuild$setName"(dependsOn: beforeBuild) { + } + + def copyJsTask = task "copyJs$setName"(dependsOn: beforeBuildTask, type: Copy) { + from "$setDir/js" + into jsDir + } + + def lintJsTask = task "lintJs$setName"(dependsOn: beforeBuildTask, type: Exec) { + inputs.dir("$setDir/js/").skipWhenEmpty(); + commandLine "eslint", '--format', 'stylish', "$setDir/js/" + } + + def compileTsTask = task "compileTs$setName"(dependsOn: beforeBuildTask, type: Exec) { + inputs.dir("$setDir/ts").skipWhenEmpty() + inputs.file("$srcDir/tsconfig.json") + inputs.file("$setDir/tsconfig.json") + outputs.dir(compileDir) + outputs.dir(declDir) + + commandLine 'node_modules/.bin/tsc', + '-p', "$setDir/tsconfig.json", + '-t', target, + '-m', jsmodule, + '-d', + '--outDir', compileDir, + '--declarationDir', declDir + + if (lib) + args '--lib', lib + } + + def copyTsOutputTask = task "copyTsOutput$setName"(dependsOn: compileTsTask, type: Copy) { + from compileDir + into jsDir + } + + def copyTypingsTask = task "copyTypings$setName"(dependsOn: compileTsTask, type: Copy) { + from declDir + into jsDir + } + + def copyResourcesTask = task "copyResources$setName"(dependsOn: beforeBuildTask, type: Copy) { + from "$setDir/resources" + into outDir + } + + task "build$setName" { + dependsOn copyTypingsTask, + copyTsOutputTask, + copyJsTask, + copyResourcesTask, + lintJsTask + } +} + +task printVersion { + doLast { + println "version: $version"; + println "isRelease: $isRelease, tagDistance: $tagDistance"; + println "packageName: $packageName"; + println "bundle: ${pack.outputs.files.join(',')}"; + println "target: $target"; + println "module: $jsmodule"; + } +} + +task clean { + doLast { + delete buildDir + delete typingsDir + } +} + +task _initBuild { + mustRunAfter clean + + def buildInfoFile = "$buildDir/platform"; + inputs.property('target',target); + inputs.property('jsmodule',jsmodule); + outputs.file(buildInfoFile); + + doLast { + delete buildDir + mkdir buildDir + + def f = new File(buildInfoFile); + f << "$target-$jsmodule"; + } +} + +task cleanNpm { + doLast { + delete 'node_modules' + } +} + +task _npmInstall() { + inputs.file("package.json") + outputs.dir("node_modules") + doLast { + exec { + commandLine 'npm', 'install' + } + } +} + +beforeBuild { + dependsOn _initBuild + dependsOn _npmInstall +} + +sourceSets.each { createSoursetTasks(it, distDir) } + +testSets.each { createSoursetTasks(it, testDir) } + +compileTsAmd { + dependsOn compileTsMain +} + +compileTsCjs { + dependsOn compileTsMain +} + +task build(dependsOn: buildMain) { + if (jsmodule == "amd") + dependsOn buildAmd + if (jsmodule == "commonjs") + dependsOn buildCjs +} + +compileTsTest { + dependsOn build +} + +compileTsTestAmd { + dependsOn compileTsTest +} + +compileTsTestCjs { + dependsOn compileTsTest +} + +task _installLocalCjsDependency(dependsOn: [buildTestCjs, "_packageMeta"], type: Exec) { + inputs.file("$distDir/package.json") + outputs.upToDateWhen { + new File("$testDir/$packageName").exists() + } + + workingDir testDir + + commandLine 'npm', 'install', '--no-save', '--force', distDir +} + +task test(dependsOn: [buildTest], type: Exec) { + if (jsmodule == "amd") + dependsOn buildTestAmd + if (jsmodule == "commonjs") { + dependsOn buildTestCjs + dependsOn _installLocalCjsDependency + } + + commandLine 'node', "$testDir/run-tests.js" +} + +task _packageMeta(type: Copy) { + mustRunAfter build + + inputs.property("version", version) + from('.') { + include '.npmignore', 'readme.md', 'license', 'history.md' + } + from("package.${jsmodule}.json") { + expand project.properties + rename { "package.json" } + } + into distDir +} + +task pack(dependsOn: [build, _packageMeta], type: Exec) { + workingDir distDir + outputs.file("$npmScope-$npmName-${version}.tgz") + + commandLine 'npm', 'pack' +} + +task publish(dependsOn: [build, _packageMeta], type: Exec) { + doFirst { + if (!isRelease) + throw new Exception("Can't publish an unreleased version"); + } + workingDir distDir + + commandLine 'npm', 'publish', '--access', 'public' +} + +task markRelease(type: Exec) { + onlyIf { tagDistance > 1 } + commandLine "hg", "tag", "v$version"; +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,9 @@ +version= +author=Implab team +jsmodule=amd +target=es5 +description=The simple framework for writing a RESTful application +license=BSD-2-Clause +repository=https://bitbucket.org/implab/implabjs-web +npmScope=implab +npmName=web \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..87b738cbd051603d91cc39de6cb000dd98fe6b02 GIT binary patch literal 55190 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
6Ty2j6jS(iuqO^~X5==7|yUymd3Y zZRst<2v1IjB*ja|NKPS2b!OSY%B)(yUWHufjYd#hE^|#dCUAofb6v=rVg#6AIv4GW z*kX|`ZEQ+}5y(I+*d~Thw%%i{VtI4RPq9v-GNB&L9B~2e&GpSb>Yn5jj&{=eY$mj| zAX2K-un)@WZes-z-+Piwg+Z0`07QU_#PVZ^wFnK=Z`Ke|;l z&DFwIEjSLC-%Y}rEv6UPAh17=u#|7;70A<1d8t`o{aB~-Mb}~1yak4P_bzkp$29{>-%w9MEfGk`90D|8!bU^+k z!=J;s`%4$(za5PO{3hP~3$7jGFX7%kubG31qocIN+e~KjKb>F#4lju2w^JDe^V{wH zzu+q{{}f)t&B)5x(Za<<)WXU{z{u#0LB_zw!0dk_h&v0Sc7HqJAt3LKqwd3>5{UiR zQx!)O2XR|#6KPvpOJ}=3hwde?+GTq?r-Y~g0F=Ljb|(4@=>IMV_7B_pbKJ*0j4u^$ zM;mXygulbRrT$Y~iNEebJO178MsL;yiSQn8o955)Y#f~otgQYg8&)o(_N957R-C8+ z06zQ<&!6S5@cta_s)zH=_N{}ML4TVyf1#a8{3)8Wt=a$B+qO2Ch|O<KErz$bTql^`7dzpvNyN z&6xj8-s2C;@;mK>UrZYD|Dlq?d#d*t>c6OTlm6{|_4g$2(|&)E*rfjZNx<(}-shM8 zVzEsBx3W#&6TDC2`$b@u^&h76y{CGgK=q4CHTU01r}~2<`7W8}7k)+lf1FzLp6q@8 z!7s9dqQ9Gs@E-I1&hsx!-j=_+5B-M=q;~=AUzk!o|F>}Vd*t_FzhB6<{r^c2@IBA_ tP}DCTp~3$kF!i41eR$*-4chSk8Z42Q0tNfWn;G7IFyCfvv*CX{`+qg-6LtUq diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/history.md b/history.md new file mode 100644 --- /dev/null +++ b/history.md @@ -0,0 +1,10 @@ +HISTORY +======= + +1.0.0 +----- + +First release, the port from legacy project, intorduces the following features + +- `security` - basic security model and concepts +- A resource-oriented web application framework \ No newline at end of file diff --git a/license b/license new file mode 100644 --- /dev/null +++ b/license @@ -0,0 +1,22 @@ +Copyright 2017-2019 Implab team + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,477 @@ +{ + "name": "@implab/web", + "version": "0.0.1-dev", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@implab/core-amd": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@implab/core-amd/-/core-amd-1.2.15.tgz", + "integrity": "sha512-Vc5L9W/jz62R2fW1RnhPd6S503oztJLgddBGgNgJ4JjaBZt9P/Ym+98jAMkFbtCY3dX1RLM0SuI33hDPoG8Wgw==", + "dev": true + }, + "@types/node": { + "version": "11.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.9.4.tgz", + "integrity": "sha512-Zl8dGvAcEmadgs1tmSPcvwzO1YRsz38bVJQvH1RvRqSR9/5n61Q1ktcDL0ht3FXWR+ZpVmXVwN1LuH4Ax23NsA==", + "dev": true + }, + "@types/requirejs": { + "version": "2.1.31", + "resolved": "https://registry.npmjs.org/@types/requirejs/-/requirejs-2.1.31.tgz", + "integrity": "sha512-b2soeyuU76rMbcRJ4e0hEl0tbMhFwZeTC0VZnfuWlfGlk6BwWNsev6kFu/twKABPX29wkX84wU2o+cEJoXsiTw==", + "dev": true + }, + "@types/tape": { + "version": "4.2.33", + "resolved": "https://registry.npmjs.org/@types/tape/-/tape-4.2.33.tgz", + "integrity": "sha512-ltfyuY5BIkYlGuQfwqzTDT8f0q8Z5DGppvUnWGs39oqDmMd6/UWhNpX3ZMh/VYvfxs3rFGHMrLC/eGRdLiDGuw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "deep-equal": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.1.2.tgz", + "integrity": "sha1-skbCuApXCkfBG+HZvRBw7IeLh84=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + }, + "dependencies": { + "object-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", + "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==", + "dev": true + } + } + }, + "defined": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-0.0.0.tgz", + "integrity": "sha1-817qfXBekzuvE7LwOz+D2SFAOz4=", + "dev": true + }, + "dojo": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/dojo/-/dojo-1.15.0.tgz", + "integrity": "sha512-+1r5Nj1+iaHI8AxUadqsSp8wJMJM6sslr3INgWKhxUA0xHznBNY0htt38XLyheuy1G7oOwsh4X1An+Uzirj7Gw==", + "dev": true + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + }, + "dependencies": { + "object-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", + "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==", + "dev": true + } + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "faucet": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/faucet/-/faucet-0.0.1.tgz", + "integrity": "sha1-WX3PHSGJosBiMhtZHo8VHtIDnZw=", + "dev": true, + "requires": { + "defined": "0.0.0", + "duplexer": "~0.1.1", + "minimist": "0.0.5", + "sprintf": "~0.1.3", + "tap-parser": "~0.4.0", + "tape": "~2.3.2", + "through2": "~0.2.3" + }, + "dependencies": { + "tape": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tape/-/tape-2.3.3.tgz", + "integrity": "sha1-Lnzgox3wn41oUWZKcYQuDKUFevc=", + "dev": true, + "requires": { + "deep-equal": "~0.1.0", + "defined": "~0.0.0", + "inherits": "~2.0.1", + "jsonify": "~0.0.0", + "resumer": "~0.0.0", + "through": "~2.3.4" + } + } + } + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", + "integrity": "sha1-16oye87PUY+RBqxrjwA/o7zqhWY=", + "dev": true + }, + "object-inspect": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", + "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", + "dev": true + }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "requirejs": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz", + "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==", + "dev": true + }, + "resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resumer": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", + "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", + "dev": true, + "requires": { + "through": "~2.3.4" + } + }, + "sprintf": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/sprintf/-/sprintf-0.1.5.tgz", + "integrity": "sha1-j4PjmpMXwaUCy324BQ5Rxnn27c8=", + "dev": true + }, + "string.prototype.trim": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", + "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.0", + "function-bind": "^1.0.2" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "tap-parser": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-0.4.3.tgz", + "integrity": "sha1-pOrhkMENdsehEZIf84u+TVjwnuo=", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~1.1.11" + } + }, + "tape": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/tape/-/tape-4.10.1.tgz", + "integrity": "sha512-G0DywYV1jQeY3axeYnXUOt6ktnxS9OPJh97FGR3nrua8lhWi1zPflLxcAHavZ7Jf3qUfY7cxcVIVFa4mY2IY1w==", + "dev": true, + "requires": { + "deep-equal": "~1.0.1", + "defined": "~1.0.0", + "for-each": "~0.3.3", + "function-bind": "~1.1.1", + "glob": "~7.1.3", + "has": "~1.0.3", + "inherits": "~2.0.3", + "minimist": "~1.2.0", + "object-inspect": "~1.6.0", + "resolve": "~1.10.0", + "resumer": "~0.0.0", + "string.prototype.trim": "~1.1.2", + "through": "~2.3.8" + }, + "dependencies": { + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", + "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", + "dev": true, + "requires": { + "readable-stream": "~1.1.9", + "xtend": "~2.1.1" + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, + "typescript": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.3.tgz", + "integrity": "sha512-Y21Xqe54TBVp+VDSNbuDYdGw0BpoR/Q6wo/+35M8PAU0vipahnyduJWirxxdxjsAkS7hue53x2zp8gz7F05u0A==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "dev": true, + "requires": { + "object-keys": "~0.4.0" + } + } + } +} diff --git a/package.amd.json b/package.amd.json new file mode 100644 --- /dev/null +++ b/package.amd.json @@ -0,0 +1,25 @@ +{ + "name": "${packageName}", + "version": "${version}", + "description": "${description}", + "main": "main.js", + "keywords": [ + "di", + "ioc", + "logging", + "template engine", + "dependency injection" + ], + "author": "${author}", + "license": "${license}", + "repository": "$repository", + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "dojo": "^1.10.0", + "@implab/core-amd": "^1.2.0" + }, + "module": "${jsmodule}", + "target": "${target}" +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "@implab/web", + "version": "0.0.1-dev", + "description": "Simple web framework", + "main": "main.js", + "keywords": [ + "di", + "ioc", + "logging", + "template engine", + "dependency injection" + ], + "author": "Implab team", + "license": "BSD-2-Clause", + "repository": "https://bitbucket.org/implab/implabjs", + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "dojo": "^1.10.0", + "@implab/core-amd": "^1.2.15", + "tslib": "latest" + }, + "devDependencies": { + "@types/node": "latest", + "@types/requirejs": "latest", + "@types/tape": "latest", + "@implab/core-amd": "^1.2.15", + "dojo": "^1.10.0", + "faucet": "latest", + "requirejs": "latest", + "tape": "^4.9.2", + "tslib": "latest", + "typescript": "latest" + }, + "types": "main.d.ts" +} diff --git a/readme.md b/readme.md new file mode 100644 --- /dev/null +++ b/readme.md @@ -0,0 +1,27 @@ +# Implabjs-core + +Набор стандартных библиотек для создания приложений со сложным функционалом. +Данную библиотеку можно использовать как для разработки приложений, которые +будут работать в среде браузеров, так и в серверных средах. + +Библиотека написана на TypeScript, некоторая часть на JavaScript, но постепенно +планируется перейти полностью на использование TypeScript + +Более подробная документация доступна по ссылке: + +## Основные компоненты + +### DI + +Контейнер для внедрения зависимостей, позволяет гибко описывать структуру +приложения и создавать слабосвязанные компоненты. + +### LOG + +Средства журналирования похожие на JLog, позволяют эффективно вести журнал +выполнения программы. + +### Cancellations + +Специальные маркеры для отмены асинхронных операций, по аналогии с .NET +CancelationToken. diff --git a/settings.gradle b/settings.gradle new file mode 100644 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,15 @@ +/* + * This settings file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * In a single project build this file can be empty or even removed. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user guide at https://docs.gradle.org/3.5/userguide/multi_project_builds.html + */ + +// To declare projects as part of a multi-project build use the 'include' method + +//include 'sub-project-name' + +rootProject.name = 'web' \ No newline at end of file diff --git a/src/main/js/BaseApplication.js b/src/main/js/BaseApplication.js new file mode 100644 --- /dev/null +++ b/src/main/js/BaseApplication.js @@ -0,0 +1,167 @@ +define([ + "dojo/_base/declare", + "dojo/_base/lang", + "dojo/when", + "cluster", + "os", + "implab/safe", + "./HttpResponse", + "./BaseResponse", + "dojo/Deferred", + "./HttpException", + "@implab/core/log/trace!"], + function (declare, lang, when, cluster, os, safe, HttpResponse, BaseResponse, Deferred, HttpException, trace) { + return declare(null, { + _container: null, + _serverPort: null, + _bindAddr: null, + + _useCluster: null, + + _requestConfig: null, + _restartFailedWorkers: null, + + // модули, обрабатывающие запрос. + _modules: null, + _handlers: null, + + constructor: function (options) { + safe.argumentNotNull(options, "options"); + safe.argumentNotNull(options.container, "options.container"); + safe.argumentNotNull(options.serverPort, "options.serverPort"); + safe.argumentNotNull(options.useCluster, "options.useCluster"); + safe.argumentNotEmptyString(options.bindAddr, "options.bindAddr"); + safe.argumentNotEmptyString(options.requestConfig, "options.requestConfig"); + + this.options = options || {}; + this._modules = []; + + this._container = options.container; + + this._serverPort = options.serverPort; + this._bindAddr = options.bindAddr; + this._useCluster = options.useCluster; + this._restartFailedWorkers = options.restartFailedWorkers; + + this._requestConfig = options.requestConfig; + + this._container.getService("httpHandlers").forEach(handler => { + this.handle(handler); + }); + }, + + start: function () { + let me = this; + const numCPUs = os.cpus().length; + if (cluster.isMaster && me._useCluster) { + // Fork workers. + for (let i = 0; i < numCPUs; i++) { + cluster.fork(); + } + + cluster.on('exit', function (worker, code, signal) { + if(me._restartFailedWorkers){ + trace.log("worker {0} died. Signal {1}. Restarting ...", worker.process.pid, signal); + cluster.fork(); + } else { + trace.log("worker {0} died.", worker.process.pid); + } + }); + } else { + // Workers can share any TCP connection + // In this case it is an HTTP server + this.listen(this._serverPort, this._bindAddr); + } + }, + + listen: function (port, host) { + this._host = host; + this._port = port; + + this._createServer(this.options, lang.hitch(this, "_handler")).listen(port, host, lang.hitch(this, "_listening")); + }, + + handle: function (module) { + if (module instanceof Function) + this._modules.push(module); + else if (module.invoke instanceof Function) + this._modules.push(function (req, next) { + return module.invoke(req, next); + }); + else + throw "module shoud be a function or should have an invoke method"; + }, + + _createServer: function () { + throw "NOT IMPLEMENTED"; + }, + + _createRequest: function (container, req, res) { + return container.configure(this._requestConfig) + .then(function () { + let httpRequest = container.getService("httpRequest"); + httpRequest.init(req, res); + return httpRequest; + }); + }, + + //handler function to pass to the approperate server (like node.http) + _handler: function (req, res) { + let i = 0; + let me = this; + + let requestContainer = this._container.createChildContainer(); + + when(this._createRequest(requestContainer, req, res), function (httpRequest) { + let next = function () { + if (i < me._modules.length) { + let module = me._modules[i]; + i++; + try { + return when(module(httpRequest, next)); + } catch (err) { + let d = new Deferred(); + d.reject(err); + return d; + } + } + }; + + when(next(), function (result) { + if (!result) + throw "no response is provided, this is a serious bug"; + + if (result instanceof BaseResponse) { + result.send(res); + } else { + let httpResp = new HttpResponse(); + httpResp.content = "BUG: " + result.toString(); + httpResp.send(res); + } + requestContainer.dispose(); + }).then(null, function (err) { + if (res.finished) { + trace.log("caught exception after the response is sent: {0}", err); + return; + } + let httpResp; + if (err instanceof HttpException) + httpResp = new HttpResponse(err.message, { + statusCode: err.code, + headers: err.headers + }); + else + httpResp = new HttpResponse(err.toString(), { + statusCode: 500 + }); + httpResp.send(res); + requestContainer.dispose(); + }); + }); + }, + _listening: function () { + trace.log("Listening {0}:{1}", this._host, this._port); + }, + + }); + }); \ No newline at end of file diff --git a/src/main/js/BaseResponse.js b/src/main/js/BaseResponse.js new file mode 100644 --- /dev/null +++ b/src/main/js/BaseResponse.js @@ -0,0 +1,7 @@ +define([ "dojo/_base/declare"], function(declare) { + return declare(null, { + send : function(/*serverResponse*/) { + throw "NOT IMPLEMENTED"; + } + }); +}); \ No newline at end of file diff --git a/src/main/js/ContentNegotiateModule.js b/src/main/js/ContentNegotiateModule.js new file mode 100644 --- /dev/null +++ b/src/main/js/ContentNegotiateModule.js @@ -0,0 +1,22 @@ +define(["dojo/_base/declare", "dojo/when", "./BaseResponse", "./ModelResponse"],function(declare,when, BaseResponse, ModelResponse) { + return declare(null,{ + invoke: function(httpRequest, next) { + return when(next(), function(result) { + if (!(result instanceof BaseResponse) ){ + result = new ModelResponse(result); + } + + if (!(result instanceof ModelResponse)) + return result; + + result.presenter = { + present: JSON.stringify, + contentType: "application/json; charset=utf-8" + }; + + // TODO select presenter and assing it to result.presenter + return result; + }); + } + }); +}); \ No newline at end of file diff --git a/src/main/js/ContentType.js b/src/main/js/ContentType.js new file mode 100644 --- /dev/null +++ b/src/main/js/ContentType.js @@ -0,0 +1,206 @@ +define([ "dojo/_base/declare", "dojo/_base/lang" ], function(declare, lang) { + let cls = declare(null, { + type : null, + subtype : null, + parameters: null, + + constructor: function(media,parameters) { + if (!media) + throw "A media must be specified"; + media = media.toString(); + let parts = media.match(/^([a-zA-Z-]+)\/([a-zA-Z\-+]+)$/); + if(!parts) + throw "An invalid media type supplied"; + this.type = parts[1]; + this.subtype = parts[2]; + this.parameters = parameters || {}; + }, + + toString : function() { + let res = [this.type,'/',this.subtype]; + for(var p in this.parameters) { + res.push('; '); + res.push(p); + res.push('='); + res.push(this._escapeValue(this.parameters[p])); + } + + return res.join(''); + }, + + _escapeValue : function(value) { + if (value) { + value = value.toString(); + if(value.match(/[()<>@,:;\\".[\]]/)) + return '"' + value.replace(/[()<>@,:;\\".[\]]/g, function(x) { return "\\"+ x; } ) + '"'; + } + return value; + } + + }); + + let parseSpace = function(text,start) { + for(var i = start; i < text.length; i++) { + if(!text[i].match(/\s/)) + break; + } + return { + pos: i + }; + }; + + let parseToken = function(text,start,required) { + let token = []; + for(var i=start; i< text.length; i++) { + let char = text[i]; + if (!char.match(/[\w-]/)) + break; + token.push(char); + } + + if (required && token.length == 0) { + if (i == text.length) + throw new Error("Unexpected end of line"); + else + throw lang.replace("Unexpected char '{char}' at '{pos}'", { char: text[i], pos: i}); + } + + return { + value : token.join(''), + pos : i + }; + }; + + + let parseMedia = function(text,start) { + let type, subtype; + let t = parseToken(text,start,true); + + type = t.value; + start = t.pos; + + if (text[start] != '/') + throw lang.replace("Unexpected char '{char}' at '{pos}'", { char: text[start], pos: start}); + start++; + + t = parseToken(text,start,true); + subtype = t.value; + start = t.pos; + + return { + value: { + type : type, + subtype: subtype + }, + pos : start + }; + }; + + let parseLiteral = function(text,pos,required) { + let data = []; + let escape, stop; + for(var i=pos; (i < text.length) && !stop; i++) { + if (escape) { + data.push(text[i]); + escape = false; + } else { + switch(text[i]) { + + case '"': + if (pos != i) + stop = true; + break; + case '\\': + escape = true; + break; + default: + if (pos == i) { + stop = true; + i--; + } else { + data.push(text[i]); + } + } + } + } + + if(required && data.length == 0) + new lang.replace("Unexpected char '{char}' at '{pos}'", { char: text[i], pos: i}); + + return { + value : data.join(''), + pos : i + }; + }; + + let parseParam = function(text,pos) { + let t,name,value; + t = parseToken(text,pos,true); + + name = t.value; + pos = t.pos; + + t = parseSpace(text,pos); + pos = t.pos; + + if (text[pos] != '=') + throw lang.replace("Unexpected char '{char}' at '{pos}'", { char: text[pos], pos: pos}); + pos++; + + t = parseSpace(text,pos); + pos = t.pos; + + t = parseToken(text,pos,false); + if (t.pos != pos) { + value = t.value; + pos = t.pos; + } else { + t = parseLiteral(text,pos,false); + if (t.pos == pos) + throw lang.replace("Unexpected char '{char}' at '{pos}'", { char: text[pos], pos: pos}); + pos = t.pos; + value = t.value; + } + + return { + value : { + name : name, + value : value + }, + pos : pos + }; + }; + + cls.parse = function(text) { + let t,pos = 0, media, params = {}; + t = parseMedia(text,pos); + media = t.value; + pos = t.pos; + + + while(pos < text.length) { + t = parseSpace(text,pos); + pos = t.pos; + + if (pos < text.length) { + if(text[pos] == ';') { + pos ++; + t = parseSpace(text,pos); + pos = t.pos; + + t = parseParam(text,pos); + + params[t.value.name] = t.value.value; + pos = t.pos; + + } else { + throw lang.replace("Unexpected char '{char}' at '{pos}'", { char: text[pos], pos: pos}); + } + } + } + + return new cls( media.type + '/' + media.subtype, params ); + }; + + return cls; +}); \ No newline at end of file diff --git a/src/main/js/Cookie.js b/src/main/js/Cookie.js new file mode 100644 --- /dev/null +++ b/src/main/js/Cookie.js @@ -0,0 +1,238 @@ +/** + * + */ +define(["dojo/_base/declare", "dojo/_base/lang", "implab/safe"], function (declare, lang, safe) { + + + var formats = { + "base64+json": { + encode: function (data) { + return new Buffer(JSON.stringify(data)).toString('base64'); + }, + decode: function (data) { + try { + return JSON.parse(new Buffer(data, 'base64').toString()); + } catch (err) { + return null; + } + } + }, + "base64": { + encode: function (data) { + if (!safe.isPrimitive(data)) + throw new Error("Can'n serialize a complex data"); + return new Buffer(data).toString('base64'); + }, + decode: function (data) { + return new Buffer(data, 'base64').toString(); + }, + }, + "simple": { + encode: function (data) { + if (!safe.isPrimitive(data)) + throw new Error("Can'n serialize a complex data"); + return safe.isNull(data) ? "" : data.toString(); + }, + decode: function (data) { + return data; + } + } + }; + + let args = { + secure: false, + httpOnly: false, + path: null, + domain: null, + maxAge: null, + expires: null, + extension: null, + format: null + }; + + let Cookie = declare(null, { + secure: false, + httpOnly: false, + path: null, + name: null, + domain: null, + maxAge: null, + expires: null, + extension: null, + value: null, + format: "base64+json", + _createTime: null, + + constructor: function (name, value, opts) { + safe.argumentNotEmptyString(name, "name"); + + if (opts) { + for (let i in opts) + if (i in args) + this[i] = opts[i]; + } + + this.name = name; + this.value = value; + this._createTime = new Date(); + }, + + isExpired: function () { + let expires; + if (this.maxAge) { + let dt = this.getNormalMaxAge(); + expires = new Date(this._createTime.getTime() + dt * 1000); + } else if (this.expires) { + expires = new Date(this.expires); + } + + return (expires && new Date() > expires); + }, + + getNormalMaxAge: function () { + if (safe.isNull(this.maxAge)) + return null; + if (safe.isNumber(this.maxAge)) + return Math.round(this.maxAge); + + let norm = Number(this.maxAge); + + if (norm == this.maxAge) + return Math.round(norm); + + let parts = this.maxAge.toString().match("^(?:(\\d+)d)?(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?$"); + if (parts) { + let factor = [0, 86400, 3600, 60, 1]; + norm = 0; + for (let i = 1; i < parts.length; i++) { + norm += factor[i] * (Number(parts[i]) || 0); + } + } else { + norm = null; + } + + return norm; + }, + + toString: function () { + let data = []; + + let pair = function (name, value, enc) { + if (!safe.isNullOrEmptyString(value)) + data.push([name, enc ? enc(value) : value].join('=')); + }; + + let flag = function (name, isSet) { + if (isSet) + data.push(name); + }; + + pair(this.name, this.value, this.format ? Cookie.formats[this.format].encode : null); + pair("Expires", this.expires, function (d) { + return new Date(d).toUTCString(); + }); + pair("Max-Age", this.getNormalMaxAge()); + pair("Domain", this.domain); + pair("Path", this.path); + flag("Secure", this.secure); + flag("HttpOnly", this.httpOnly); + flag(this.extension, this.extension); + + return data.join('; '); + } + + }); + + /** + * Парсит строку с печеньками из HTTP заголока Cookies + * + * @return возвращает коллекцию печенек в виде { "cookie-name" : { + * value='raw%20data', decode : function(format) {} } } + * + */ + Cookie.parseHeaderValue = function (str) { + safe.argumentNotEmptyString(str, "str"); + + let pairs = str.split(/;\s*/); + let cookies = {}; + + for (let i in pairs) { + let pair = pairs[i]; + let idx = pair.indexOf('='); + if (idx >= 0) { + cookies[pair.substring(0, idx)] = { + value: pair.substring(idx + 1), + decode: function (format) { + if (!format) + format = "simple"; + + if (!formats[format]) + throw new Error("The specified format '" + format + "' is unsupported"); + let decode = formats[format].decode; + + return decode(this.value); + } + }; + } + } + + return cookies; + + }; + + Cookie.formats = formats; + + Cookie.parse = function (str, format) { + safe.argumentNotEmptyString(str, "str"); + if (!format) + format = "simple"; + + if (!formats[format]) + throw new Error("The specified format '" + format + "' is unsupported"); + + let pairs = str.split(/;\s*/); + let options = { + format: format + }; + + let cookieName, cookieValue, first = true; + + let map = { + "Expires": "expires", + "Max-Age": "maxAge", + "Domain": "domain", + "Path": "path", + "Secure": "secure", + "HttpOnly": "httpOnly" + }; + + for (let i in pairs) { + let pair = pairs[i]; + let idx = pair.indexOf('='); + + if (idx >= 0) { + + let name = pair.substring(0, idx); + let value = pair.substring(idx + 1); + + if (first) { + cookieName = name; + cookieValue = value; + first = false; + } else { + if (name in map) + options[map[name]] = value; + } + } else { + if (pair in map) + options[map[pair]] = true; + else + options.extension = pair; + } + } + + return new Cookie(cookieName, cookieValue, options); + }; + + return Cookie; +}); \ No newline at end of file diff --git a/src/main/js/ForbiddenException.js b/src/main/js/ForbiddenException.js new file mode 100644 --- /dev/null +++ b/src/main/js/ForbiddenException.js @@ -0,0 +1,6 @@ +define(["dojo/_base/declare", "./HttpException"], function (declare, httpException) { + return declare([httpException], { + code: 403, + message: "Forbidden" + }); +}); diff --git a/src/main/js/HelloModule.js b/src/main/js/HelloModule.js new file mode 100644 --- /dev/null +++ b/src/main/js/HelloModule.js @@ -0,0 +1,9 @@ +define(["dojo/_base/declare","./HttpResponse"],function(declare,HttpResponse) { + return declare(null,{ + invoke: function(/*httpRequest, next*/) { + return new HttpResponse("Hello, world!",{ + contentType : "text/plain; charset=utf-8" + }); + } + }); +}); \ No newline at end of file diff --git a/src/main/js/HttpApplication.js b/src/main/js/HttpApplication.js new file mode 100644 --- /dev/null +++ b/src/main/js/HttpApplication.js @@ -0,0 +1,11 @@ +define([ + "dojo/_base/declare", + "./BaseApplication", + "dojo/node!http" +], function(declare, base, http) { + return declare(base, { + _createServer : function(options, handler) { + return http.createServer(handler); + } + }); +}); \ No newline at end of file diff --git a/src/main/js/HttpException.js b/src/main/js/HttpException.js new file mode 100644 --- /dev/null +++ b/src/main/js/HttpException.js @@ -0,0 +1,7 @@ +define(["dojo/_base/declare"], function (declare) { + return declare(null, { + code: 500, + message: "Internal server error", + headers: null + }); +}); diff --git a/src/main/js/HttpRequest.js b/src/main/js/HttpRequest.js new file mode 100644 --- /dev/null +++ b/src/main/js/HttpRequest.js @@ -0,0 +1,123 @@ +define([ + "dojo/_base/declare", + "dojo/Deferred", + "url", + "implab/safe", + "./Cookie", + "./ContentType", + "implab/log/trace!"], function (declare, Deferred, url, safe, Cookie, ContentType, trace) { + return declare([], { + + // http.IncommingMessage + message: null, + + response: null, + + session: null, + + cookieFormat: "base64+json", + _contentType: null, + _container: null, + _initialised: false, + + constructor: function (options) { + safe.argumentNotNull(options, "options"); + safe.argumentNotNull(options.container, "options.container"); + this._container = options.container; + + trace.log("Request created"); + }, + + init: function (message, response) { + safe.argumentNotNull(message, "message"); + + this.message = message; + this.method = message.method; + this.path = url.parse(message.url, true, true).pathname; + + this.response = response; + + this._initialised = true; + }, + + _checkInit: function () { + if(!this._initialised){ + throw new Error("Request is not initialised. 'init' function should be called"); + } + }, + + getService: function (service) { + return this._container.getService(service); + }, + + getContainer: function () { + return this._container; + }, + + getRemoteAddress: function () { + return this.message.socket.remoteAddress; + }, + + readAllText: function (encoding) { + this._checkInit(); + if (!encoding) + encoding = this.getContentType().parameters.charset || 'utf8'; + this.message.setEncoding(encoding); + + let d = new Deferred(); + + let chunks = []; + + this.message.on('data', function (chunk) { + chunks.push(chunk); + }); + this.message.on('end', function () { + d.resolve(chunks.join('')); + }); + this.message.on('error', function (e) { + d.reject(e); + }); + return d; + }, + + header: function (name) { + safe.argumentNotEmptyString(name, "name"); + this._checkInit(); + + return this.message.headers[name.toLowerCase()]; + }, + + getContentType: function () { + this._checkInit(); + if (safe.isNull(this._contentType)) + this._contentType = ContentType.parse(this.header('Content-Type')); + return this._contentType; + }, + /** Возвращает значение печеньки из запроса. + * @name Имя печеньки + * @format Формат значения, например json+base64 + * @return Объект со значением печеньки + */ + cookie: function (name, format) { + safe.argumentNotEmptyString(name, "name"); + this._checkInit(); + + if (!format) + format = this.cookieFormat; + + if (safe.isNull(this._cookies)) { + let cookiesHeader = this.header("Cookie"); + if (!safe.isNullOrEmptyString(cookiesHeader)) + this._cookies = Cookie.parseHeaderValue(cookiesHeader); + else + this._cookies = {}; + } + + let cookie = this._cookies[name]; + if (safe.isNull(cookie)) + return null; + + return cookie.decode(format); + } + }); +}); \ No newline at end of file diff --git a/src/main/js/HttpResponse.js b/src/main/js/HttpResponse.js new file mode 100644 --- /dev/null +++ b/src/main/js/HttpResponse.js @@ -0,0 +1,137 @@ +define([ "dojo/_base/declare", "dojo/_base/lang", "implab/safe" ,"./ContentType", "./BaseResponse", "./Cookie" ], function(declare, lang,safe, ContentType, BaseResponse, Cookie) { + return declare(BaseResponse, { + statusCode : 0, + _headers : null, + content : null, + _contentType : null, + _cookies : null, + + getContentType : function() { + return this._contentType; + }, + + setContentType : function(value) { + if (value) { + this._contentType = value instanceof ContentType ? value : ContentType.parse(value.toString()); + this._headers["Content-Type"] = this._contentType.toString(); + } else { + this._contentType = null; + delete this._headers["Content-Type"]; + } + this._headers["Content-Type"] = value ? value.toString() : value; + }, + + /** + * Записывает cookies + * @cookie {String|Cookie} печенька или имя печеньки или печенька сохраненная в строку. + * @value? {*} если {cookie} строка с именем, то в этом параметре передается значение. + * @options? {Object} если {cookie} строка с именем, то в этом параметре передаются дополнительные свойства печенек. + */ + setCookie : function(cookie /*, value, options*/) { + if (arguments.length == 1) { + if (safe.isString(cookie)) + cookie = Cookie.parse(cookie); + safe.argumentOfType(cookie, Cookie, "cookie"); + this._cookies[cookie.name] = cookie; + } else { + safe.argumentNotEmptyString(cookie,"cookie"); + let value = arguments[1]; + let options = arguments[2]; + + let cc = new Cookie(cookie,value,options); + + this._cookies[cc.name] = cc; + } + }, + + _setCookies : function(cookies) { + if (!(cookies instanceof Array)) + cookies = [cookies]; + let me = this; + cookies.forEach(function(cookie){ + me.setCookie(cookie); + }); + }, + + forgetCookie : function(name) { + this.setCookie(new Cookie(name,"__erased__", { maxAge : 0 })); + }, + + setHeader : function(name, value) { + if (!name) + throw "A name is requried"; + name = this._normalizeHeaderName(name); + + if (name == "Content-Type") + this.setContentType(value); + if (name == "Set-Cookie") + this._setCookies(value); + else + this._headers[name] = value; + }, + + getHeader : function(name) { + if (!name) + throw "A name is requried"; + name = this._normalizeHeaderName(name); + + return this._headers[name]; + }, + + _normalizeHeaderName : function(name) { + return name.toLowerCase().replace(/\b\w/g, function(s) { + return s.toUpperCase(); + }); + }, + + constructor : function(content, options) { + this._headers = {}; + this._cookies = {}; + + this.content = content; + this.statusCode = content ? 200 : 203; + + if (options) { + if ("statusCode" in options) + this.statusCode = options.statusCode; + if ("headers" in options) { + for ( let header in options.headers) + this.setHeader(header, options.headers[header]); + } + if ("contentType" in options) + this.setContentType(options.contentType); + } + }, + + send : function(serverResponse) { + if (!serverResponse) + throw "ServerResponse is required"; + + this.writeHead(serverResponse); + + this.writeEntity(serverResponse); + + serverResponse.end(); + }, + + writeHead : function(serverResponse) { + let headers = lang.clone(this._headers); + let cookies = []; + for (let key in this._cookies) + cookies.push(this._cookies[key].toString()); + headers["Set-Cookie"] = cookies; + + serverResponse.writeHead(this.statusCode, headers); + }, + + writeEntity : function(serverResponse) { + if (this.content instanceof Function) { + this.content(serverResponse); + } else if (this.content) { + serverResponse.write(this.content, this._contentType ? this._contentType.parameters.charset : undefined); + } + } + + + }); +}); \ No newline at end of file diff --git a/src/main/js/MediaTypes.js b/src/main/js/MediaTypes.js new file mode 100644 --- /dev/null +++ b/src/main/js/MediaTypes.js @@ -0,0 +1,93 @@ +define([], function() { + return { + application : { + atom: 'application/atom+xml', + json : 'application/json', + javascript: 'application/javascript', + octetStream: 'application/octet-stream', + ogg: 'application/ogg', + pdf: 'application/pdf', + soap: 'application/soap+xml', + xhtml: 'application/xhtml+xml', + dtd: 'application/xml-dtd', + zip: 'application/zip', + gzip: 'application/x-gzip', + form: 'application/x-www-form-urlencoded', + ttf: 'application/x-font-ttf', + tar: 'application/x-tar', + pkcs12: 'application/x-pkcs12', + pfx: 'application/x-pkcs12', + spc: 'application/x-pkcs7-certificates', + p7b: 'application/x-pkcs7-certificates', + p7r: 'application/x-pkcs7-certreqresp', + p7c: 'application/x-pkcs7-mime', + p7s: 'application/x-pkcs7-signature' + }, + audio : { + mulaw: 'audio/basic', + pcm24: 'audio/L24', + mp4: 'audio/mp4', + mp3: 'audio/mpeg', + mpeg: 'audio/mpeg', + ogg: 'audio/ogg', + vorbis: 'audio/vorbis', + wma: 'audio/x-ms-wma', + wmaRedirect: 'audio/x-ms-wax', + realAudio: 'audio/vnd.rn-realaudio', + wav: 'audio/vnd.wave', + webm: 'audio/webm' + }, + image: { + gif: 'image/gif', + jpeg: 'image/jpeg', + msJpeg: 'image/pjpeg', + png: 'image/png', + svg: 'image/svg+xml', + tiff: 'image/tiff', + ico: 'image/vnd.microsoft.icon', + wbmp: 'image/vnd.wap.wbmp', + bmp: 'image/bmp' + }, + message : { + http: 'message/http', + imdn: 'message/imdn+xml', + emailPartial: 'message/partial', + email: 'message/rfc822' + }, + model : { + example: 'model/example', + iges: 'model/iges', + mesh: 'model/mesh', + vrml: 'model/vrml', + x3db: 'model/x3d+binary', + x3dv: 'model/x3d+vrml', + x3d: 'model/x3d+xml' + }, + multipart: { + mixed: 'multipart/mixed', + alternative: 'multipart/alternative', + related: 'multipart/related', + form: 'multipart/form-data', + signed: 'multipart/signed', + encrypted: 'multipart/encrypted' + }, + text : { + cmd: 'text/cmd', + css: 'text/css', + csv: 'text/csv', + html: 'text/html', + plain: 'text/plain', + xml: 'text/xml' + }, + video: { + mpeg: 'video/mpeg', + mp4: 'video/mp4', + ogg: 'video/ogg', + quicktime: 'video/quicktime', + webm: 'video/webm', + wmv: 'video/x-ms-wmv', + flv: 'video/x-flv' + } + + }; +}); \ No newline at end of file diff --git a/src/main/js/ModelResponse.js b/src/main/js/ModelResponse.js new file mode 100644 --- /dev/null +++ b/src/main/js/ModelResponse.js @@ -0,0 +1,29 @@ +define([ "dojo/_base/declare", "./HttpResponse" ], function(declare, HttpResponse) { + return declare(HttpResponse, { + presenter : null, + + constructor : function(model, options) { + if (options && options.presenter) + this.presenter = options.presenter; + }, + + send : function() { + console.log("try to present: " + this.content); + if (!this.presenter) + throw "The presenter isn't specified for the model '" + this.content + "'"; + + if (this.statusCode != 203 && this.presenter.contentType) + this.setHeader("Content-type", this.presenter.contentType); + + this.inherited(arguments); + + }, + + writeEntity : function(serverResponse) { + if (this.statusCode == 203) + return; + + serverResponse.write(this.presenter.present(this.content), this.getContentType() ? this.getContentType().parameters.charset : undefined); + } + }); +}); \ No newline at end of file diff --git a/src/main/js/NotAllowedException.js b/src/main/js/NotAllowedException.js new file mode 100644 --- /dev/null +++ b/src/main/js/NotAllowedException.js @@ -0,0 +1,6 @@ +define(["dojo/_base/declare", "./HttpException"], function (declare, httpException) { + return declare([httpException], { + code: 405, + message: "Method Not Allowed" + }); +}); diff --git a/src/main/js/NotFoundException.js b/src/main/js/NotFoundException.js new file mode 100644 --- /dev/null +++ b/src/main/js/NotFoundException.js @@ -0,0 +1,6 @@ +define(["dojo/_base/declare", "./HttpException"], function (declare, httpException) { + return declare([httpException], { + code: 404, + message: "Not found" + }); +}); diff --git a/src/main/js/Resource.js b/src/main/js/Resource.js new file mode 100644 --- /dev/null +++ b/src/main/js/Resource.js @@ -0,0 +1,129 @@ +define([ + "dojo/_base/declare", + "dojo/_base/lang", + "dojo/when", + "implab/safe", + "./NotAllowedException", + "./HttpResponse" +], function ( + declare, + lang, + when, + safe, + NotAllowedException, + HttpResponse +) { + let resource = declare(null, { + + // instance members + request: null, + + // родительский ресурс + parent: null, + + // имя текущего ресурса, является фрагментом пути к запрашиваемому + // ресурсу + name: null, + + allowedMethods: { + "head": 0, + "options": 0, + "get": 0, + "post": 0, + "put": 1, // extended method + "delete": 1 // extended method + }, + + constructor: function (options) { + if (options) { + declare.safeMixin(this, options); + } + }, + + accessCheck: null, + + getAllowedMethods: function (cors) { + let methods = []; + for (var m in this.allowedMethods) { + if (m in this && (cors && this.allowedMethods[m] || !cors)) + methods.push(m.toUpperCase()); + } + return methods; + }, + + options: function () { + let resp = new HttpResponse(null, { + headers: { + "Access-Control-Allow-Methods": this.getAllowedMethods(true), + "Allowed-Methods": this.getAllowedMethods() + } + }); + return resp; + }, + + invoke: function () { + let method = this.request.method.toLowerCase(); + + let me = this; + + if (!(method in this)) { + throw new NotAllowedException(); // TODO: Возвратить список возможных методов (verbs) + } + + if (!safe.isNull(this.accessCheck)) { + return when(this.request.session, function (session) { + me.session = session; + return when(me.accessCheck(), function () { + return me[method](); + }); + }, function (err) { + console.log(err); + throw err; + }); + } else { + return this[method](); + } + }, + + render: function (view, model, mimeType) { + return function (resp) { + if (mimeType) + resp.type(mimeType); + resp.render(view, { + model: model + }); + }; + + }, + + getChild: function (name) { + if (this.children && name in this.children) { + let child = this.children[name]; + + if (typeof child == "function") { + return child({ + request: this.request, + parent: this, + name: name + }); + } else if (child.hasOwnProperty("isInstanceOf") && child.isInstanceOf(resource)) { + return lang.mixin(child, { + request: this.request, + parent: this, + name: name + }); + } else { + return new resource(lang.mixin(child, { + request: this.request, + parent: this, + name: name + })); + } + } else { + return null; + } + } + }); + + return resource; +}); \ No newline at end of file diff --git a/src/main/js/RestHandler.js b/src/main/js/RestHandler.js new file mode 100644 --- /dev/null +++ b/src/main/js/RestHandler.js @@ -0,0 +1,50 @@ +define([ + "dojo/_base/declare", + "dojo/_base/lang", + "dojo/when", + "./NotFoundException", + "./HttpException", + "./Resource", + "implab/safe" +], + function (declare, lang, when, notFoundException, httpException, resource, safe) { + + return declare([], { + _resourcesConfig: null, + + constructor: function (options) { + safe.argumentNotNull(options, "options"); + safe.argumentNotNull(options.resourcesConfig, "options.resourcesConfig"); + + this._resourcesConfig = options.resourcesConfig; + }, + + createResource: function (req) { + let container = req.getContainer(); + return container.configure(this._resourcesConfig) + .then(function(){ + let rc = container.getService("resource"); + + if(rc.hasOwnProperty("isInstanceOf") && rc.isInstanceOf(resource)) { + return rc; + } else { + return new resource(lang.mixin(rc,{request: req})); + } + }); + }, + + invoke: function (req/*, next*/) { + return when(this.createResource(req), function (rc) { + req.path.split(/\/+/).forEach(function (child) { + if (child) { + rc = rc.getChild(child); + if (!rc) { + throw new notFoundException(); + } + } + }); + return rc.invoke(); + }); + } + }); + }); diff --git a/src/main/js/StubResponse.js b/src/main/js/StubResponse.js new file mode 100644 --- /dev/null +++ b/src/main/js/StubResponse.js @@ -0,0 +1,23 @@ +define([ "dojo/_base/declare", "./BaseResponse" ], function(declare, BaseResponse) { + /** + * Заглушка вместо ответа сервера, используется при использовании "родных" модулей + * node, требующи работы напрямую с ответом, для чего у HttpRequest запрашивается + * свйоство response, а вместо ответа возвращается заглушка. + */ + return declare(BaseResponse, { + _serverResponse : null, + + constructor : function(serverResponse) { + this._serverResponse = serverResponse; + }, + + setHeader : function() { + this._serverResponse.setHeader.apply(this._serverResponse, arguments); + }, + + send : function(serverResponse) { + if (!serverResponse.finished) + serverResponse.end(); + } + }); +}); \ No newline at end of file diff --git a/src/main/js/main.js b/src/main/js/main.js new file mode 100644 --- /dev/null +++ b/src/main/js/main.js @@ -0,0 +1,18 @@ +/** + * Created by internet on 6/21/16. + */ +'user strict'; +/*===== + return { + // summary: + // The implab package main module; implab package is somewhat unusual in that the main module currently just provides an empty object. + // Apps should require modules from the implab packages directly, rather than loading this module. + }; + =====*/ + +/** + The entry point + @module implab + */ + +module.exports = {}; \ No newline at end of file diff --git a/src/main/js/security/AuthCode.js b/src/main/js/security/AuthCode.js new file mode 100644 --- /dev/null +++ b/src/main/js/security/AuthCode.js @@ -0,0 +1,13 @@ +/** + * Created by andrei on 22.08.16. + */ +define(['dojo/_base/declare'], function(declare){ + let AuthCode = declare(null, { + }); + + AuthCode.SUCCSESS = 0; + AuthCode.INCMPLETE = 1; + AuthCode.FAIL = 2; + + return AuthCode; +}); diff --git a/src/main/js/security/CookieSecurityAuthority.js b/src/main/js/security/CookieSecurityAuthority.js new file mode 100644 --- /dev/null +++ b/src/main/js/security/CookieSecurityAuthority.js @@ -0,0 +1,93 @@ +/** + * Модуль, отвечающий за создание контекста безопасности (сессии). + * + * Идентификатор сессии сохраняется в печеньках на клиенте. + */ +define([ + "dojo/_base/declare", + "dojo/when", + "implab/safe", + "../Cookie", + "../security/SecData", + "../BaseResponse", + "implab/log/trace!" +], function (declare, when, safe, Cookie, SecData, BaseResponse, trace) { + let COOKIE_NAME = "ssid"; + + return declare(null, { + _request : null, + _provider : null, + _cookies : null, + _sessionData: null, + + constructor : function(options) { + safe.argumentNotNull(options, "options"); + safe.argumentNotNull(options.securityProvider, "options.securityProvider"); + safe.argumentNotNull(options.request, "options.request"); + + this._request = options.request; + this._provider = options.securityProvider; + this._cookies = []; + }, + + /** + * Вызывается из {SecurityHandler} по окончании обработки запроса. + * + * @resp ответ сервера + */ + completeRequest : function(resp) { + trace.log("completeRequest"); + if (resp instanceof BaseResponse) + this._cookies.forEach(function(cookie) { + resp.setCookie(cookie); + }); + if (!safe.isNull(this._sessionData)) + return this._sessionData.save().then(function () { + return resp; + }); + return resp; + }, + + handleError : function(err) { + throw err; + }, + + initSession : function(userIdentity) { + safe.argumentNotNull(userIdentity, "userIdentity"); + let me = this; + + return when(me._provider.getSessions().createSession(userIdentity, SecData.newSSID()), function(session) { + if (!session) { + throw new Error("Can`t init session"); + } + + me._sessionData = session; + trace.log("Created session {0} for user {1}", session.sessionId, userIdentity.getUser().login); + me._cookies.push(new Cookie(COOKIE_NAME,session.sessionId)); + return session; + }); + }, + + /* aync */ + getSession : function() { + let cookie; + let me = this; + try { + cookie = this._request.cookie(COOKIE_NAME); + + if (!cookie) + return null; + + return when(me._provider.getSessions().getSession(cookie), function(session) { + me._sessionData = session; + return session; + }); + } catch (e) { + trace.log("getSession: {0}", e); + return null; + } + } + + }); + +}); \ No newline at end of file diff --git a/src/main/js/security/Identity.js b/src/main/js/security/Identity.js new file mode 100644 --- /dev/null +++ b/src/main/js/security/Identity.js @@ -0,0 +1,81 @@ +/** + * @class Identity идентификационная информация (удостоверение) пользователя. + * Описывает пользователя и способ его аутентификации в системе. Данный + * класс является заглушкой, описывающей интерфейс, а также может + * использоваться для создания идентификацинной информации вручную. + */ +define([ + "dojo/_base/declare", + "implab/safe", + "./SecData" +], function (declare, safe, SecData) { + let identity = declare(null, { + _user : null, + + _isAnonymous : null, + + _secData : null, + + constructor : function(user, opts) { + this._user = user; + safe.mixin(this, opts, { + isAnonymous : '_isAnonymous', + secData : '_secData' + }); + }, + + getUser : function() { + return this._user; + }, + + getSecData : function() { + return this._secData; + }, + + /** + * Проводит раунд аутентификации, изменяет текущее состояние. Делегирует + * выполнение процедуры объекту SecData. + * + * @param challenge + * {*} - данные для аутентификации, зависит от реализации, + * например, пароль. + * @retuns authResult {Object} - результат аутентификации { challenge : + * 'response data', code : AUTH_* }. + * @throws {Error} + * в случае ошибки или если модуль аутентификации не задан. + */ + doAuth : function(challenge) { + if (this._secData) + return this._secData.doAuth(challenge); + + throw new Error("Authentication is not available for this object"); + }, + + getAuthType : function() { + if (this._secData) + return this._secData.getAuthType(); + + return null; + }, + + getIsAuthenticated : function() { + if (this._secData) { + return this._secData.getAuthSate() == SecData.AUTH_SUCCESS; + } + return false; + }, + + getIsAnonymous : function() { + return this._isAnonymous; + }, + + getAuthState : function() { + if (this._secData) + return this._secData.getAuthState(); + + throw new Error("Authentication is not available for this object"); + } + }); + + return identity; +}); \ No newline at end of file diff --git a/src/main/js/security/NoneSecData.js b/src/main/js/security/NoneSecData.js new file mode 100644 --- /dev/null +++ b/src/main/js/security/NoneSecData.js @@ -0,0 +1,18 @@ +define([ + "dojo/_base/declare", + "./SecData" +], function (declare, SecData) { + return declare(SecData, { + doAuth: function (/*challenge*/) { + return { + code: SecData.AUTH_SUCCESS + }; + }, + getAuthState: function () { + return SecData.AUTH_SUCCESS; + }, + getAuthType: function () { + return "none"; + } + }); +}); \ No newline at end of file diff --git a/src/main/js/security/SecData.js b/src/main/js/security/SecData.js new file mode 100644 --- /dev/null +++ b/src/main/js/security/SecData.js @@ -0,0 +1,73 @@ +define([ + "dojo/_base/declare", "dojo/node!crypto"], function (declare, crypto) { + let SecData = declare(null, { + _state: null, + _token: null, + _authType: null, + + constructor: function (authType) { + this._authType = authType; + }, + + /** + * Проводит раунд аутентификации, изменяет текущее состояние. + * @param challenge {*} - данные для аутентификации, зависит от реализации, например, пароль. + * @retuns authResult {Object} - результат аутентификации { challenge : 'response data', code : AUTH_* }. + */ + doAuth: function (challenge) { + let password = challenge; + + if (this.validateHash(password)) { + this._state = SecData.AUTH_SUCCESS; + } else { + this._state = SecData.AUTH_FAIL; + } + + return { + code: this._state + }; + + }, + getAuthState: function () { + return this._state; + }, + getAuthType: function () { + return this._authType; + }, + + generateHash: function (password) { + return md5hex(password); + }, + parse: function (token) { + this._token = token; + }, + + validateHash: function (password) { + return this.generateHash(password) == this._token; + } + }); + + + SecData.AUTH_SUCCESS = 0; + SecData.AUTH_INCOMPLETE = 1; + SecData.AUTH_FAIL = 2; + + + function md5hex() { + let md5 = crypto.createHash('md5'); + + for (let i = 0; i < arguments.length; i++) + md5.update(String(arguments[i])); + + return md5.digest('hex'); + } + + let i = 0; + + SecData.md5hex = md5hex; + SecData.newSSID = function () { + return md5hex(new Date().getTime(), Math.random(), i++); + }; + + return SecData; +}); \ No newline at end of file diff --git a/src/main/js/security/SecurityHandler.js b/src/main/js/security/SecurityHandler.js new file mode 100644 --- /dev/null +++ b/src/main/js/security/SecurityHandler.js @@ -0,0 +1,56 @@ +/** + * Обработчик системы безопасности, встраивается в стек обработки запросов и + * контролирует процесс создания контекста безопасности регистрирует в запросе + * сервис с именем session. + * + * Для получения и создания контекста безопасности используется + * SecurityAuthority которое отвечает за механизмы проверки подлинности и + * доверенность полученной сессии, по сути SecurityAuthority реализует протокол + * безопасности. + * + * При первом доступе к сервисам возможна прозрачная аутентификация, при этом + * создается новая сессия, для ее создания используется IdentityProvider. + * IdentityProvider - внешний модуль аутентификации, который по требованию + * предоставляет идентификатор пользователя, используя собственные механизмы. + * + * После получения данных аутентификации от IdentityProvider они используются в + * SecurityAuthority для создания сессии. + * + * Многоэтапная аутентификация сессии - случай пока чисто теоретический, оданко, + * в случае необходимости SecurityAuthtority вызывает исключение при получении + * сесии, если требуется следующий этап аутентификации сессии, данное исключение + * прерывает текущую обработку запроса и попадает в обработчик + * SecurityAuthority.handleError который в свою очередь формирует ответ сервера + * для продолжения аутентификации. + */ +define([ "dojo/_base/declare", "dojo/when"], + function(declare, when) { + + return declare(null, { + constructor : function(/*options*/) { + }, + + /** + * Точка входа в обработчик, вызывается инфраструктурой при + * обработке запроса. + * + * @req Текущий запрос. + * @next Следующий обработчик в цепочке, вызывается когда сессия уже + * зарегистрирована в локаторе. + * @async + */ + invoke : function(req, next) { + let authority = req.getService("securityAuthority"); + return when(next(), function(resp) { + if (authority) + return authority.completeRequest(resp); + return resp; + }, function(err) { + if (authority) + return authority.handleError(err); + else + throw err; + }); + } + }); + }); diff --git a/src/main/js/security/SecurityServices.js b/src/main/js/security/SecurityServices.js new file mode 100644 --- /dev/null +++ b/src/main/js/security/SecurityServices.js @@ -0,0 +1,31 @@ +/** + * Created by andrei on 21.08.16. + */ +define([ + "dojo/_base/declare", + "implab/safe", + "./NoneSecData", + "./SecData" +], function(declare, safe, NoneSecData, SecData) { + let SecurityServices = declare(null, { + }); + + SecurityServices.getSecurityDataService = function(tokenType) { + switch (tokenType) { + case this.PASSWORD_AUTH_TYPE: + return new SecData(tokenType); + case this.SATISFY_ANY_AUTH_TYPE: + return new NoneSecData(); + case this.REJECT_ALL_AUTH_TYPE: + throw Error("NotImplemented"); + default: + throw Error("Unsupported auth type " + tokenType); + } + + }; + SecurityServices.PASSWORD_AUTH_TYPE = "password"; + SecurityServices.SATISFY_ANY_AUTH_TYPE = "satisfyany"; + SecurityServices.REJECT_ALL_AUTH_TYPE = "rejectall"; + + return SecurityServices; +}); diff --git a/src/main/js/security/Session.js b/src/main/js/security/Session.js new file mode 100644 --- /dev/null +++ b/src/main/js/security/Session.js @@ -0,0 +1,101 @@ +/** + * Created by andrei on 08.04.16. + */ +define([ + "dojo/_base/declare", + "dojo/_base/lang", + "dojo/when", + "implab/safe", + "../ForbiddenException"], + function (declare, lang, when, safe, ForbiddenException) { + return declare(null, { + _container: null, + _sessionData: null, + /** + * Создавать анонимную сессию. Если прозрачная аутентификация не + * проводилась, и текущей сессии нет, то может быть создана сессия + * для анонимного пользователя. Это сделано опцией, поскольку не во + * всех системах требуется, отслеживать активность анонимных + * пользователей (это не бесплатно). + */ + _createAnonymousSession: false, + + /** + * Регистрировать пользователей, полученных из внешнего источника + * аутентификации. + */ + _autoRegisterUsers: false, + + constructor: function (options) { + safe.argumentNotNull(options, "options"); + safe.argumentNotNull(options.container, "options.container"); + + this._container = options.container; + + this._createAnonymousSession = options.createAnonymousSession || this._createAnonymousSession; + this._autoRegisterUsers = options.autoRegisterUsers || this._autoRegisterUsers; + + this._sessionData = this._initSession(); + }, + getSessionData: function () { + return this._sessionData; + }, + + /** + * @remarks Сосздание/восстановление сессии происходит в несколько + * этапов 1. Получаем текущую сессию. 2. Если упешно, то + * выходим. 3. Получаем идентификатор пользователя, + * используя identityProvider 4. Если успешно 5. Создаем + * новую сессию для полученного пользователя, выходим 6. + * Иначе если установлен параметр _createAnonymousSession 7. + * Создаем новую анонимную сессию. 8. Иначе, выходим + */ + _initSession: function () { + let me = this; + + let securityAuthority = me._container.getService("securityAuthority"); + let securityProvider = me._container.getService("securityProvider"); + let identityProvider = me._container.getService("identityProvider"); + + if (!securityAuthority) + throw new Error("Sessions are not supported, no security authority is supplied"); + + + // пытаемся получить текущую сессию + return when(securityAuthority.getSession(), function (session) { + if (session) + return session; + + // пытаемся создать сессию с использованием + // пользователя из внешнего + // источника аутентификации + if (identityProvider) { + return when(identityProvider.getUserLogin(), function (login) { + if (login) { + return when(securityProvider.createUserIdentity(login, false, { + createUser: me._autoRegisterUsers + }), function (uid) { + return securityAuthority.initSession(uid); + }, function (err) { + console.log("Failed to create session for '" + login + "' ", err); + throw new ForbiddenException(); + }); + } else if (me._createAnonymousSession) { + return when(securityProvider.getAnonymousIdentity(), function (uid) { + return securityAuthority.initSession(uid); + }); + } else { + throw new ForbiddenException(); + } + }); + } else if (me._createAnonymousSession) { + return when(securityProvider.getAnonymousIdentity(), function (uid) { + return securityAuthority.initSession(uid); + }); + } + + throw new ForbiddenException(); + }); + } + }); + }); \ No newline at end of file diff --git a/tslint.json b/tslint.json new file mode 100644 --- /dev/null +++ b/tslint.json @@ -0,0 +1,40 @@ +{ + "extends": "tslint:recommended", + "rules": { + "align": [ + true, + "parameters", + "statements" + ], + "interface-name": [false], + "max-line-length": [ true, 185 ], + "member-access": false, + "member-ordering": [ + false, + "variables-before-functions" + ], + "no-bitwise": false, + "no-empty": false, + "no-namespace": false, + "no-string-literal": false, + "ordered-imports": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-whitespace" + ], + "object-literal-sort-keys": false, + "trailing-comma": [ + true, + { + "singleline": "never", + "multiline": "never" + } + ], + "variable-name": false, + "curly": false, + "array-type": false, + "arrow-parens": [true, "ban-single-arg-parens"] + } +} \ No newline at end of file