pom 依赖如下,使用的 solon 版本为 2.8.4
1 2 3 4 5 6
| <parent> <groupId>org.noear</groupId> <artifactId>solon-parent</artifactId> <version>2.8.4</version> <relativePath /> </parent>
|
请求的处理流程图可以参考官方文档
![[_resources/solon 框架请求处理流程分析/15abb79c9f150557722aa3a43977549f_MD5.jpeg]]
这里以一个 postController 举例来分析处理请求的过程
1 2 3 4 5 6 7 8 9 10 11
| @Mapping("/api") @Post public String api(Map map, Context ctx) throws Exception { JSONObject jsonObject = new JSONObject(ctx.body()); if (map.size() != jsonObject.length()) { User user = (User)deserialize((String)map.get("data")); return user.getName(); } else { return "success"; } }
|
ActionExecuteHandlerDefault
这里用 burp 发送一个正常的 post 请求
1 2 3 4 5 6 7
| POST /api HTTP/1.1 Host: 192.168.13.1:8888 Connection: keep-alive Content-Type: application/x-www-form-urlencoded Content-Length: 9
a=b&map=1
|
入口从org.noear.solon.core.route.RouterHandler#handleMain 开始,直到调用 org.noear.solon.core.mvc.ActionDefault#executeDo 方法
1 2 3 4 5
| protected Object executeDo(Context c, Object obj) throws Throwable { ActionExecuteHandler executeHandler = Solon.app().chainManager() .getExecuteHandler(c, mWrap.getParamWraps().length); return executeHandler.executeHandle(c, obj, mWrap); }
|
继续跟进 getExecuteHandler,此时executeHandlers 中一个 org.noear.solon.serialization.snack 3.SnackActionExecutor,用于处理 json 请求
![[_resources/solon 框架请求处理流程分析/94de6414cc075fde9b95d4cfdeb63003_MD5.jpeg]]
但是这里需要 contentType 满足一定条件,最终定位到org.noear.solon.serialization.snack 3.SnackStringSerializer#label 这个变量,contentType 需要包含 /json
才可以使用 SnackActionExecutor 来进行处理,否则会调用getExecuteHandlerDefault 来获取默认的 Executor,也就是 org.noear.solon.core.mvc.ActionExecuteHandlerDefault
buildArgs
下一步会进入 executeHandle
1 2 3 4 5
| @Override public Object executeHandle(Context ctx, Object obj, MethodWrap mWrap) throws Throwable { List<Object> args = buildArgs(ctx, obj, mWrap); return mWrap.invokeByAspect(obj, args.toArray()); }
|
会首先进行 buildArgs,用于处理请求的相关参数
调用 changeBody 来从 SmHttpContext 从获取 body
![[_resources/solon 框架请求处理流程分析/1cda61ba9c937792dc49f42569b5f251_MD5.jpeg]] 最终在org.noear.solon.boot.smarthttp.http.SmHttpContext#paramsMapInit 中进行处理,使用_request.getParameters()获取 body,最终返回一个 NvMap 类型。
接着会获取 controller 中请求参数的类型,并用 bodyObj 来进行转换,如果参数是以下类型,会直接进行获取,不会使用 bodyObj
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| if (Context.class.isAssignableFrom(pt)) { args.add(ctx); } else if (ModelAndView.class.isAssignableFrom(pt)) { args.add(new ModelAndView()); } else if (Locale.class.isAssignableFrom(pt)) { args.add(ctx.getLocale()); } else if (UploadedFile.class == pt) { }
|
如果不是以上的类型,会继续调用 org.noear.solon.core.handle.Context#pull
1 2 3 4 5 6 7 8 9 10 11 12
| public Object pull(Class<?> clz) { if (clz.isInstance(request())) { return request(); } if (clz.isInstance(response())) { return response(); } if (clz.isInstance(sessionState())) { return sessionState(); } return null; }
|
如果还没有获取到,就会开始进入分支,先判断是否为 isRequiredBody,是的话会进行赋值,例如 Map 类型会直接将刚刚的 bodyObj 赋值过去
1 2 3 4 5 6 7 8 9 10 11 12
| if (tv == null) { if (p.isRequiredBody()) { if (String.class.equals(pt)) { tv = ctx.bodyNew(); } else if (InputStream.class.equals(pt)) { tv = ctx.bodyAsStream(); } else if (Map.class.equals(pt) && bodyObj instanceof NvMap) { tv = bodyObj; } } }
|
如果不是 RequiredBody,则会调用 changeValue 进行参数的类型转换,把 bodyObj 转换成目标所需的类型
1 2 3 4 5 6 7 8 9
| if (tv == null) { try { tv = changeValue(ctx, p, i, pt, bodyObj); } catch (Exception e) { String methodFullName = mWrap.getDeclaringClz().getName() + "::" + mWrap.getName() + "@" + p.getName(); throw new StatusException("Action parameter change failed: " + methodFullName, e, 400); } }
|
如果经过上述步骤还没拿到 tv,就会判断是否是基础类型,并赋值一些初始值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| if (tv == null) { if (pt.isPrimitive()) { if (pt == short.class) { tv = (short) 0; } else if (pt == int.class) { tv = 0; } else if (pt == long.class) { tv = 0L; } else if (pt == double.class) { tv = 0d; } else if (pt == float.class) { tv = 0f; } else if (pt == boolean.class) { tv = false; } else { throw new IllegalArgumentException("Please enter a valid parameter @" + p.getName()); } } }
|
changeValue
我们重点关注类型转换的过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| protected Object changeValue(Context ctx, ParamWrap p, int pi, Class<?> pt, Object bodyObj) throws Exception { String pn = p.getName(); String pv = p.getValue(ctx); Object tv = null; if (pv == null) { pv = p.getDefaultValue(); } if (pv == null) { if (UploadedFile.class == pt) { tv = ctx.file(pn); } else if (UploadedFile[].class == pt) { tv = Utils.toArray(ctx.files(pn), new UploadedFile[]{}); } else { if (pn.startsWith("$")) { tv = ctx.attr(pn); } else { if (pt.getName().startsWith("java.") || pt.isArray() || pt.isPrimitive()) { tv = null; } else { tv = changeEntityDo(ctx, p, pn, pt); } } } } else { tv = changeValueDo(ctx, p, pn, pt, pv); } return tv; }
|
第一种参数的获取方式是先获取参数的名字,并尝试从 ctx 中拿到对应的值
1 2
| String pn = p.getName(); //参数名 String pv = p.getValue(ctx); //参数值
|
例如我们请求中的参数 Map map
,对应的 pn 就为 map
,我们传参如果传入 map=1&a=222
就会被获取
第二种方式是 p.getDefaultValue();
获取
第三种方式首先获取文件,如果是$开头就从 attr 里面找,如果参数类型是 java.开头就交给上一层的基础类型来处理
第三种方式中最终会使用 changeEntityDo 来还原对象,而通过前两种方式获取的会用 changeValueDo 来还原
changeValueDo
具体转换的逻辑在 org.noear.solon.core.util.ConvertUtil#to(org.noear.solon.core.wrap.VarDescriptor, java.lang.String, org.noear.solon.core.handle.Context)中
前面基本都是一些基本类型的转换,最后会进入 org.noear.solon.core.util.ConvertUtil#tryTo 中进行转换
这个函数用于将 String 值转换为别的类型
1 2 3 4 5 6 7
| public static Object tryTo(Class<?> type, String val) { Converter converter = Solon.app().converterManager().find(String.class, type); if (converter != null) { return converter.convert(val); } ......
|
会从converterManager 寻找合适的转换器,但默认配置下是空的,如果需要自定义转换器可以参考官方文档进行配置 https://solon.noear.org/article/567
这一部分暂时没有什么利用方式,进入 changeValueDo 之后基本就只能进行基本类型的转换
changeEntityDo
再来看第三种方式获取的参数值如何进行转换,只能处理那些 UploadFile 类型或者不为 java.开头的类型
例如下面这个路由,入参为 User
1 2 3 4 5
| @Mapping("/addUser") @Post public String addUser(User u) { return u.getName(); }
|
会调用 org.noear.solon.core.wrap.ClassWrap#newBy(java.util.function.Function<java.lang.String,java.lang.String>, org.noear.solon.core.handle.Context)
并且调用 org.noear.solon.core.wrap.ClassWrap#doFill 去填充字段值,会将 String 的值转换成对应参数的类型,和上面的流程类似
SnackActionExecutor
我们可以引入依赖
1 2 3 4
| <dependency> <groupId>org.noear</groupId> <artifactId>solon-api</artifactId> </dependency>
|
其中包含了
1 2 3 4 5
| <!-- Json序列化支持组件 --> <dependency> <groupId>org.noear</groupId> <artifactId>solon.serialization.snack3</artifactId> </dependency>
|
用于处理 json 的序列化和反序列化,这也是官方默认配置的插件,可以参考 https://solon.noear.org/article/family-solon-serialization
我们发送以下数据
1 2 3 4 5 6 7
| POST /api HTTP/1.1 Host: 192.168.13.1:8888 Connection: keep-alive Content-Type: /json Content-Length: 12
{"test":"2"}
|
changeBody
流程和上面一样,区别在于这里重写了 changeBody
1 2 3 4
| @Override protected Object changeBody(Context ctx, MethodWrap mWrap) throws Exception { return serializer.deserializeFromBody(ctx); }
|
主要是 snack3 中的逻辑,会将我们 body 的字符串转换为 json 对象
changeValue
重写了 changeValue
进入 org.noear.snack.to.ObjectToer#analyse 来进行 json 反序列化
这里支持使用 org.noear.snack.to.ObjectToer#getTypeByNode 来指定目标类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| private Class<?> getTypeByNode(Context ctx, ONode o, Class<?> def) { if (ctx.target_type == null) { if (o.isObject()) { return LinkedHashMap.class; } if (o.isArray()) { return ArrayList.class; } } String typeStr = null; if(ctx.options.hasFeature(Feature.DisableClassNameRead) == false) { if (o.isArray() && o.ary().size() == 2) { ONode o1 = o.ary().get(0); if (o1.isObject() && o1.obj().size() == 1) { ONode n1 = o1.obj().get(ctx.options.getTypePropertyName()); if (n1 != null) { typeStr = n1.val().getString(); } } } if (o.isObject()) { ONode n1 = o.obj().get(ctx.options.getTypePropertyName()); if (n1 != null) { typeStr = n1.val().getString(); } } } if (StringUtil.isEmpty(typeStr) == false) { if(typeStr.startsWith("sun.") || typeStr.startsWith("com.sun.") || typeStr.startsWith("javax.") || typeStr.startsWith("jdk.")) { throw new SnackException("Unsupported type, class: " + typeStr); } Class<?> clz = ctx.options.loadClass(typeStr); if (clz == null) { throw new SnackException("Unsupported type, class: " + typeStr); } else { return clz; } } else { if (def == null || def == Object.class) { if (o.isObject()) { return LinkedHashMap.class; } if (o.isArray()) { return ArrayList.class; } } return def; } }
|
其中 ctx.options.getTypePropertyName()
结果就是 @type
但是需要关闭 Feature.DisableClassNameRead 这个属性,才可以从 body 中获取 @type
后续会取到属性值进行 setValue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public void setValue(Object tObj, Object val, boolean useSetter) { if (readonly) { return; } try { if (_setter != null && useSetter) { _setter.invoke(tObj, new Object[]{val}); } else { if (field.isAccessible() == false) { field.setAccessible(true); } field.set(tObj, val); } } catch (IllegalArgumentException ex) { if (val == null) { throw new IllegalArgumentException(field.getName() + "(" + field.getType().getSimpleName() + ") Type receive failure!", ex); } throw new IllegalArgumentException( field.getName() + "(" + field.getType().getSimpleName() + ") Type receive failure :val(" + val.getClass().getSimpleName() + ")", ex); } catch (IllegalAccessException e) { throw new SnackException(e); } catch (RuntimeException e) { throw e; } catch (Throwable e) { throw new RuntimeException(e); } }
|
useSetter 属性默认是 false,没有办法调用,默认只能使用反射去设置属性值,如果利用sun.print.UnixPrintServiceLookup 这个类就可能产生 rce 风险