GithubHelp home page GithubHelp logo

pedant / safe-java-js-webview-bridge Goto Github PK

View Code? Open in Web Editor NEW
2.4K 122.0 570.0 1.72 MB

为WebView中的Java与JavaScript提供【安全可靠】的多样互通方案

Home Page: http://pedant.github.io/2014/07/04/webview-js-java-interface-research/

Java 19.78% JavaScript 80.22%

safe-java-js-webview-bridge's Issues

动态注入的js内容怎么查看?

通过view.loadUrl("javascript:(function(b){fuction test(){}})(window))注入的类容注入到webview的哪个地方,在注入完成后通过document.getElementsByTagName('html')[0].innerHTML获取不到注入的类容。

import error

JsCallback.java:
import java.lang.ref.SoftReference;
->
import java.lang.ref.WeakReference;

建议

在很多时候用webview打开别人的网页需要向网页中注入js代码,以检测某些操作,可不可以把这部分功能也加入进去呢?
还有个疑问,HostJsScope类中方法的多少会不会影响效率或是页面打开的速度呢?

我把你的JsCallJava.java修改了一下,这样使用起来更加灵活:(你可以参考一下)

我把你的JsCallJava.java修改了一下,这样使用起来更加灵活:(你可以参考一下)

new MyInjectedChromeClient(new JsCallJava.InjectObj("myjs.ui", MyJs.class),new JsCallJava.InjectObj("myjs.ui2", MyJs2.class,MyJs3.class,MyJs4.class))

package cn.pedant.SafeWebViewBridge;

import android.text.TextUtils;
import android.webkit.WebView;
import android.util.Log;

import com.google.gson.Gson;

import org.json.JSONArray;
import org.json.JSONObject;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;

public class JsCallJava
{
private final static String TAG = "JsCallJava";
private final static String RETURN_RESULT_FORMAT = "{"code": %d, "result": %s}";
private HashMap<String, Method> mMethodsMap;
/////private String mInjectedName;/////
private String mPreloadInterfaceJS;
private Gson mGson;

/////
public static class InjectObj
{
    String namespace;
    Class<?>[] interfaceClasses;

    public InjectObj(String namespace, Class<?>... interfaceClasses)
    {
        if (TextUtils.isEmpty(namespace))
        {
            throw new RuntimeException("namespace can not be null!");
        }
        this.namespace = namespace;
        this.interfaceClasses = interfaceClasses;
    }
}
/////

public JsCallJava(InjectObj... injectObjs)
{
    try
    {

        mMethodsMap = new HashMap<String, Method>();
        StringBuilder sbuilder = new StringBuilder("javascript:");

        for (InjectObj injectObj : injectObjs)
        {
            injectOne(sbuilder, injectObj);
        }

        mPreloadInterfaceJS = sbuilder.toString();
    } catch (Exception e)
    {
        e.printStackTrace();
        throw new RuntimeException("init js error:" + e.getMessage());
    }
}

private void injectOne(StringBuilder sbuilder, InjectObj injectObj)
{
    String mInjectedName = injectObj.namespace;

    StringBuilder sb = new StringBuilder("(function(b){console.log(\"");/////去掉前面的javascript://////

    sb.append(mInjectedName);
    sb.append(
            " initialization begin\");var a={");

    sb.append("namespace:\"").append(mInjectedName).append("\",");//////添加一个namespace///////

    sb.append("queue:[],callback:function(){var d=Array.prototype.slice.call" +
            "(arguments,0);var c=d.shift();var e=d.shift();this.queue[c].apply(this,d);if(!e){delete " +
            "this.queue[c]}}};");


    ///////
    for (Class<?> c : injectObj.interfaceClasses)
    {
        searchClass(sb, c);
    }

    StringBuilder namespaces = new StringBuilder();

    {
        StringBuilder temp = new StringBuilder();
        String[] ss = injectObj.namespace.split("\\.");
        for (String s : ss)
        {
            if ("".equals(s))
            {
                continue;
            } else
            {
                temp.append(".").append(s);
                namespaces.append("b").append(temp).append("=").append("b").append(temp).append("||{};");
            }
        }
    }

    //////

    sb.append("function(){var f=Array.prototype.slice.call(arguments,0);if(f.length<1){throw\"");
    sb.append(mInjectedName);
    sb.append(
            " call error, message:miss method name\"}var e=[];for(var h=1;h<f.length;h++){var c=f[h];var " +
                    "j=typeof c;e[e.length]=j;if(j==\"function\"){var d=a.queue.length;a.queue[d]=c;" +
                    "f[h]=d}}var g=JSON.parse(prompt(JSON.stringify({method:f.shift(),types:e,args:f");

    sb.append(",namespace:a.namespace");/////////加入namespace/////////

    sb.append("})));if(g" +
            ".code!=200){throw\"");
    sb.append(mInjectedName);
    sb.append(
            " call error, code:\"+g.code+\", message:\"+g.result}return g.result};Object.getOwnPropertyNames" +
                    "(a).forEach(function(d){var c=a[d];if(typeof c===\"function\"&&d!==\"callback\")" +
                    "{a[d]=function(){return c.apply(a,[d].concat(Array.prototype.slice.call(arguments,0)))" +
                    "}}});");

    sb.append(namespaces);//////////加入,如:b.ui={};b.ui.abc={};

    sb.append("b.").append(mInjectedName);
    sb.append("=a;console.log(\"");
    sb.append(mInjectedName);
    sb.append(" initialization end\")})(window);");

    /////////
    sbuilder.append(sb);
    ////////
}

private void searchClass(StringBuilder sb, Class<?> c)
{
    /////个人建议还是用getMethods,这样可以不用把所有的static函数都挤在一个类里,而可以把一部分放在父类中.//////
    Method[] methods = c.getMethods();

    for (Method method : methods)
    {
        String sign;
        if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (sign = genJavaMethodSign(
                method)) == null)
        {
            continue;
        }
        mMethodsMap.put(sign, method);
        sb.append(String.format("a.%s=", method.getName()));
    }
}

private String genJavaMethodSign(Method method)
{
    String sign = method.getName();
    Class[] argsTypes = method.getParameterTypes();
    int len = argsTypes.length;
    if (len < 1 || argsTypes[0] != WebView.class)
    {
        Log.w(TAG, "method(" + sign + ") must use webview to be first parameter, will be pass");
        return null;
    }
    for (int k = 1; k < len; k++)
    {
        Class cls = argsTypes[k];
        if (cls == String.class)
        {
            sign += "_S";
        } else if (cls == int.class ||
                cls == long.class ||
                cls == float.class ||
                cls == double.class)
        {
            sign += "_N";
        } else if (cls == boolean.class)
        {
            sign += "_B";
        } else if (cls == JSONObject.class)
        {
            sign += "_O";
        } else if (cls == JsCallback.class)
        {
            sign += "_F";
        } else
        {
            sign += "_P";
        }
    }
    return sign;
}

public String getPreloadInterfaceJS()
{
    return mPreloadInterfaceJS;
}

public String call(WebView webView, String jsonStr)
{
    if (!TextUtils.isEmpty(jsonStr))
    {
        try
        {
            JSONObject callJson = new JSONObject(jsonStr);

            String mInjectedName = callJson.getString("namespace");//////得到namespace///////

            String methodName = callJson.getString("method");
            JSONArray argsTypes = callJson.getJSONArray("types");
            JSONArray argsVals = callJson.getJSONArray("args");
            String sign = methodName;
            int len = argsTypes.length();
            Object[] values = new Object[len + 1];
            int numIndex = 0;
            String currType;

            values[0] = webView;

            for (int k = 0; k < len; k++)
            {
                currType = argsTypes.optString(k);
                if ("string".equals(currType))
                {
                    sign += "_S";
                    values[k + 1] = argsVals.isNull(k) ? null : argsVals.getString(k);
                } else if ("number".equals(currType))
                {
                    sign += "_N";
                    numIndex = numIndex * 10 + k + 1;
                } else if ("boolean".equals(currType))
                {
                    sign += "_B";
                    values[k + 1] = argsVals.getBoolean(k);
                } else if ("object".equals(currType))
                {
                    sign += "_O";
                    values[k + 1] = argsVals.isNull(k) ? null : argsVals.getJSONObject(k);
                } else if ("function".equals(currType))
                {
                    sign += "_F";
                    values[k + 1] = new JsCallback(webView, mInjectedName, argsVals.getInt(k));
                } else
                {
                    sign += "_P";
                }
            }

            Method currMethod = mMethodsMap.get(sign);

            // 方法匹配失败
            if (currMethod == null)
            {
                return getReturn(jsonStr, 500, "not found method(" + sign + ") with valid parameters");
            }
            // 数字类型细分匹配
            if (numIndex > 0)
            {
                Class[] methodTypes = currMethod.getParameterTypes();
                int currIndex;
                Class currCls;
                while (numIndex > 0)
                {
                    currIndex = numIndex - numIndex / 10 * 10;
                    currCls = methodTypes[currIndex];
                    if (currCls == int.class)
                    {
                        values[currIndex] = argsVals.getInt(currIndex - 1);
                    } else if (currCls == long.class)
                    {
                        //WARN: argsJson.getLong(k + defValue) will return a bigger incorrect number
                        values[currIndex] = Long.parseLong(argsVals.getString(currIndex - 1));
                    } else
                    {
                        values[currIndex] = argsVals.getDouble(currIndex - 1);
                    }
                    numIndex /= 10;
                }
            }

            return getReturn(jsonStr, 200, currMethod.invoke(null, values));
        } catch (Exception e)
        {
            //优先返回详细的错误信息
            if (e.getCause() != null)
            {
                return getReturn(jsonStr, 500, "method execute error:" + e.getCause().getMessage());
            }
            return getReturn(jsonStr, 500, "method execute error:" + e.getMessage());
        }
    } else
    {
        return getReturn(jsonStr, 500, "call data empty");
    }
}

private String getReturn(String reqJson, int stateCode, Object result)
{
    String insertRes;
    if (result == null)
    {
        insertRes = "null";
    } else if (result instanceof String)
    {
        result = ((String) result).replace("\"", "\\\"");
        insertRes = "\"" + result + "\"";
    } else if (!(result instanceof Integer)
            && !(result instanceof Long)
            && !(result instanceof Boolean)
            && !(result instanceof Float)
            && !(result instanceof Double)
            && !(result instanceof JSONObject))
    {    // 非数字或者非字符串的构造对象类型都要序列化后再拼接
        if (mGson == null)
        {
            mGson = new Gson();
        }
        insertRes = mGson.toJson(result);
    } else
    {  //数字直接转化
        insertRes = String.valueOf(result);
    }
    String resStr = String.format(RETURN_RESULT_FORMAT, stateCode, insertRes);
    ////////Log.d(TAG, mInjectedName + " call json: " + reqJson + " result:" + resStr);
    return resStr;
}

}

WebView默认接口searchBoxJavaBridge_依然存在js注入漏洞

       您好,我发现该库没有移除WebView的默认接口searchBoxJavaBridge_,使用该默认接口依然可以通过js注入漏洞,注入方法可以参考文章中第三小节中的js方法execute。
       在使用该库之前调用webView.removeJavascriptInterface("searchBoxJavaBridge_");应该就可以解决这个问题。

请教一个问题

您好,能请教一下,你的这种实现方式比在shouldOverrideUrlLoading拦截js再去处理的优势在哪儿吗

很多机型上注入不成功

您好,进度大于25%才进行注入,在某些机型上注入不成功,或者偶尔成功偶尔不成功,这个方案不稳定啊。能再优化不? 目前验证注入失败机型:小米 MIUI-JLB52.0 android4.1.1 偶尔成功机型:索尼android4.4.2版本

发现问题

关于网页调用玩Java后回调js的问题,例如弹出个dialog点击确定后后回调js,但却不成功

我把你的JsCallJava的代码改了一下,用起来会更加灵活:(你可以参考一下)

我把你的代码改了一下,用起来会更加灵活:(你可以参考一下)
new MyInjectedChromeClient(new JsCallJava.InjectObj("myjs.ui", MyJs.class),
new JsCallJava.InjectObj("myjs.ui2", MyJs2.class,MyJs3.class,MyJs4.class));

package cn.pedant.SafeWebViewBridge;

import android.text.TextUtils;
import android.webkit.WebView;
import android.util.Log;

import com.google.gson.Gson;

import org.json.JSONArray;
import org.json.JSONObject;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;

public class JsCallJava
{
private final static String TAG = "JsCallJava";
private final static String RETURN_RESULT_FORMAT = "{"code": %d, "result": %s}";
private HashMap<String, Method> mMethodsMap;
/////<private String mInjectedName;>/////
private String mPreloadInterfaceJS;
private Gson mGson;

/////<
public static class InjectObj
{
    String namespace;
    Class<?>[] interfaceClasses;

    public InjectObj(String namespace, Class<?>... interfaceClasses)
    {
        if (TextUtils.isEmpty(namespace))
        {
            throw new RuntimeException("namespace can not be null!");
        }
        this.namespace = namespace;
        this.interfaceClasses = interfaceClasses;
    }
}
/////>

public JsCallJava(InjectObj... injectObjs)
{
    try
    {

        mMethodsMap = new HashMap<String, Method>();
        StringBuilder sbuilder = new StringBuilder("javascript:");

        for (InjectObj injectObj : injectObjs)
        {
            injectOne(sbuilder, injectObj);
        }

        mPreloadInterfaceJS = sbuilder.toString();
    } catch (Exception e)
    {
        e.printStackTrace();
        throw new RuntimeException("init js error:" + e.getMessage());
    }
}

private void injectOne(StringBuilder sbuilder, InjectObj injectObj)
{
    String mInjectedName = injectObj.namespace;

    StringBuilder sb = new StringBuilder("(function(b){console.log(\"");/////去掉前面的javascript://////

    sb.append(mInjectedName);
    sb.append(
            " initialization begin\");var a={");

    sb.append("namespace:\"").append(mInjectedName).append("\",");//////添加一个namespace///////

    sb.append("queue:[],callback:function(){var d=Array.prototype.slice.call" +
            "(arguments,0);var c=d.shift();var e=d.shift();this.queue[c].apply(this,d);if(!e){delete " +
            "this.queue[c]}}};");


    //////////////<
    for (Class<?> c : injectObj.interfaceClasses)
    {
        searchClass(sb, c);
    }

    StringBuilder namespaces = new StringBuilder();

    {
        StringBuilder temp = new StringBuilder();
        String[] ss = injectObj.namespace.split("\\.");
        for (String s : ss)
        {
            if ("".equals(s))
            {
                continue;
            } else
            {
                temp.append(".").append(s);
                namespaces.append("b").append(temp).append("=").append("b").append(temp).append("||{};");
            }
        }
    }

    ///////////>

    sb.append("function(){var f=Array.prototype.slice.call(arguments,0);if(f.length<1){throw\"");
    sb.append(mInjectedName);
    sb.append(
            " call error, message:miss method name\"}var e=[];for(var h=1;h<f.length;h++){var c=f[h];var " +
                    "j=typeof c;e[e.length]=j;if(j==\"function\"){var d=a.queue.length;a.queue[d]=c;" +
                    "f[h]=d}}var g=JSON.parse(prompt(JSON.stringify({method:f.shift(),types:e,args:f");

    sb.append(",namespace:a.namespace");/////////加入namespace/////////

    sb.append("})));if(g" +
            ".code!=200){throw\"");
    sb.append(mInjectedName);
    sb.append(
            " call error, code:\"+g.code+\", message:\"+g.result}return g.result};Object.getOwnPropertyNames" +
                    "(a).forEach(function(d){var c=a[d];if(typeof c===\"function\"&&d!==\"callback\")" +
                    "{a[d]=function(){return c.apply(a,[d].concat(Array.prototype.slice.call(arguments,0)))" +
                    "}}});");

    sb.append(namespaces);//////////加入,如:b.ui={};b.ui.abc={};

    sb.append("b.").append(mInjectedName);
    sb.append("=a;console.log(\"");
    sb.append(mInjectedName);
    sb.append(" initialization end\")})(window);");

    /////////
    sbuilder.append(sb);
    ////////
}

private void searchClass(StringBuilder sb, Class<?> c)
{
    /////个人建议还是用getMethods,这样可以不用把所有的static函数都挤在一个类里,而可以把一部分放在父类中.//////
    Method[] methods = c.getMethods();

    for (Method method : methods)
    {
        String sign;
        if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (sign = genJavaMethodSign(
                method)) == null)
        {
            continue;
        }
        mMethodsMap.put(sign, method);
        sb.append(String.format("a.%s=", method.getName()));
    }
}

private String genJavaMethodSign(Method method)
{
    String sign = method.getName();
    Class[] argsTypes = method.getParameterTypes();
    int len = argsTypes.length;
    if (len < 1 || argsTypes[0] != WebView.class)
    {
        Log.w(TAG, "method(" + sign + ") must use webview to be first parameter, will be pass");
        return null;
    }
    for (int k = 1; k < len; k++)
    {
        Class cls = argsTypes[k];
        if (cls == String.class)
        {
            sign += "_S";
        } else if (cls == int.class ||
                cls == long.class ||
                cls == float.class ||
                cls == double.class)
        {
            sign += "_N";
        } else if (cls == boolean.class)
        {
            sign += "_B";
        } else if (cls == JSONObject.class)
        {
            sign += "_O";
        } else if (cls == JsCallback.class)
        {
            sign += "_F";
        } else
        {
            sign += "_P";
        }
    }
    return sign;
}

public String getPreloadInterfaceJS()
{
    return mPreloadInterfaceJS;
}

public String call(WebView webView, String jsonStr)
{
    if (!TextUtils.isEmpty(jsonStr))
    {
        try
        {
            JSONObject callJson = new JSONObject(jsonStr);

            String mInjectedName = callJson.getString("namespace");//////得到namespace///////

            String methodName = callJson.getString("method");
            JSONArray argsTypes = callJson.getJSONArray("types");
            JSONArray argsVals = callJson.getJSONArray("args");
            String sign = methodName;
            int len = argsTypes.length();
            Object[] values = new Object[len + 1];
            int numIndex = 0;
            String currType;

            values[0] = webView;

            for (int k = 0; k < len; k++)
            {
                currType = argsTypes.optString(k);
                if ("string".equals(currType))
                {
                    sign += "_S";
                    values[k + 1] = argsVals.isNull(k) ? null : argsVals.getString(k);
                } else if ("number".equals(currType))
                {
                    sign += "_N";
                    numIndex = numIndex * 10 + k + 1;
                } else if ("boolean".equals(currType))
                {
                    sign += "_B";
                    values[k + 1] = argsVals.getBoolean(k);
                } else if ("object".equals(currType))
                {
                    sign += "_O";
                    values[k + 1] = argsVals.isNull(k) ? null : argsVals.getJSONObject(k);
                } else if ("function".equals(currType))
                {
                    sign += "_F";
                    values[k + 1] = new JsCallback(webView, mInjectedName, argsVals.getInt(k));
                } else
                {
                    sign += "_P";
                }
            }

            Method currMethod = mMethodsMap.get(sign);

            // 方法匹配失败
            if (currMethod == null)
            {
                return getReturn(jsonStr, 500, "not found method(" + sign + ") with valid parameters");
            }
            // 数字类型细分匹配
            if (numIndex > 0)
            {
                Class[] methodTypes = currMethod.getParameterTypes();
                int currIndex;
                Class currCls;
                while (numIndex > 0)
                {
                    currIndex = numIndex - numIndex / 10 * 10;
                    currCls = methodTypes[currIndex];
                    if (currCls == int.class)
                    {
                        values[currIndex] = argsVals.getInt(currIndex - 1);
                    } else if (currCls == long.class)
                    {
                        //WARN: argsJson.getLong(k + defValue) will return a bigger incorrect number
                        values[currIndex] = Long.parseLong(argsVals.getString(currIndex - 1));
                    } else
                    {
                        values[currIndex] = argsVals.getDouble(currIndex - 1);
                    }
                    numIndex /= 10;
                }
            }

            return getReturn(jsonStr, 200, currMethod.invoke(null, values));
        } catch (Exception e)
        {
            //优先返回详细的错误信息
            if (e.getCause() != null)
            {
                return getReturn(jsonStr, 500, "method execute error:" + e.getCause().getMessage());
            }
            return getReturn(jsonStr, 500, "method execute error:" + e.getMessage());
        }
    } else
    {
        return getReturn(jsonStr, 500, "call data empty");
    }
}

private String getReturn(String reqJson, int stateCode, Object result)
{
    String insertRes;
    if (result == null)
    {
        insertRes = "null";
    } else if (result instanceof String)
    {
        result = ((String) result).replace("\"", "\\\"");
        insertRes = "\"" + result + "\"";
    } else if (!(result instanceof Integer)
            && !(result instanceof Long)
            && !(result instanceof Boolean)
            && !(result instanceof Float)
            && !(result instanceof Double)
            && !(result instanceof JSONObject))
    {    // 非数字或者非字符串的构造对象类型都要序列化后再拼接
        if (mGson == null)
        {
            mGson = new Gson();
        }
        insertRes = mGson.toJson(result);
    } else
    {  //数字直接转化
        insertRes = String.valueOf(result);
    }
    String resStr = String.format(RETURN_RESULT_FORMAT, stateCode, insertRes);
    ////////Log.d(TAG, mInjectedName + " call json: " + reqJson + " result:" + resStr);
    return resStr;
}

}

发现问题

发现一问题,JsCallback回调时报空指针错误:
Caused by: java.lang.NullPointerException
at android.webkit.WebViewClassic.loadUrlImpl(WebViewClassic.java:2678)
at android.webkit.WebViewClassic.loadUrlImpl(WebViewClassic.java:2694)
at android.webkit.WebViewClassic.loadUrl(WebViewClassic.java:2687)
at android.webkit.WebView.loadUrl(WebView.java:804)
at cn.pedant.SafeWebViewBridge.JsCallback.apply(JsCallback.java:42)
at com.jidian.android.edo.util.HostJsScope$2.onComplete(HostJsScope.java:137)

我猜测可能是这样的,因为回调有延迟,callback.apply()的时候可能webview已经被关闭,导致的这个错误

请教个问题

在webview中加载的网页时常调用注入的js对象不存在,这个是什么问题呢,非网页加载完毕立即执行

JS怎么传方法给JAVA进行执行

在 JsCallJava的call方法中 有一个类型判断:values[k + 1] = new JsCallback(webView, mInjectedName, argsVals.getInt(k));进行执行的时候有一个argsVals.getInt(k)获取到对应的全局方法索引,但是假如我需要传入一个对应的方法名称(String 类型),需要怎么做,我没有看到onJsPrompt代码是怎么对message进行封装的,例如我检测到的:
message="{"method":"delayJsCallBack1","types":["number","string","function"],"args":[1,"call back haha","1"]}";其中1是message传过来的,请问我需要怎么做,期待您的回复。

为什么不用gson呢

大部分人的项目中都用了gson,如果这个项目集成gson,那么就不需要Jackson了,省点空间

JsCallback.apply(String对象)出错

Log:I/chromium: [INFO:CONSOLE(1)] "Uncaught SyntaxError: Unexpected identifier", source: (1)。
我在调用jsCallback.apply()时,传入一个String对象,在apply方法里,有这样的代码:

StringBuilder sb = new StringBuilder();
for (Object arg : args){
    sb.append(",");
    boolean isStrArg = arg instanceof String;
    if (isStrArg) {
        sb.append("\"");
    }
    sb.append(String.valueOf(arg));
    if (isStrArg) {
        sb.append("\"");
    }
}

此方法会在参数首尾加上双引号。此时当传入的String对象包含双引号时(JSONObject.toString()方法生成的String对象就会包含双引号),就会出错。不包含则不会出现问题。
将append里面的双引号改成单引号能解决此问题。但是如果String对象包含单引号就又会导致这个问题了- -
延伸到既包含双引号又包含单引号的String对象又该如何传递呢?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.