• tomcat8.5.57源码阅读笔记5.1


    上一篇终于找到了调用管道方法的地方, 代码片段:

    //org/apache/catalina/connector/CoyoteAdapter#service
    // Parse and set Catalina and configuration specific
    // request parameters 在 Map 中解析请求
    postParseSuccess = postParseRequest(req, request, res, response);
    if (postParseSuccess) {
        //check valves if we support async
        //getContainer() -> StandardEngine
        request.setAsyncSupported(
                connector.getService().getContainer().getPipeline().isAsyncSupported());
        // Calling the container
        // 调用 StandardEngineValve#invoke(), 开始执行管道方法.    (责任链模式)
        // 如果有多个 valve , 这里 getFirst() 拿到的就是 first valve,
        // 然后在 first valve中, 需要调用 getNext().invoke(request, response) 来传递
        connector.getService()   // --> StandardService
                .getContainer()  // --> StandardEngine
                .getPipeline()  // --> StandardPipeLine
                .getFirst()  //获取优先级: first > basic
                .invoke(request, response);
    }

    1. 从代码上看, 只获取了 first, 那说好的很多阀门呢? 带着疑问, 只能去看一下 StandardPipeLine 的阀门.

    2. 默认情况下, 这里的getFirst() 获取到的是 StandardEngineValve, 其实他是 basic.   

    StandardPipeLine 

      他有两个 阀门属性:  

    1. basic -- 末位阀门

    2. first -- 首尾阀门

    除此之外, 没找到任何阀门属性了. 没办法, 只能接着看看阀门

    Valve

    Valve 里面定义了两个方法:

    1. public Valve getNext()

    2. public void setNext(Valve valve)

    看到这里, 总算明白了, 原来阀门本身, 也是一个指针, 指向了下一个阀门的位置. 使用阀门, 可以组成一个单向链表.

    addValve()

    明白了阀门是指针之后, 还需要知道, 增加阀门时的顺序, 所以必须看一下 addValve() 方法.

    /**
     * 加入valve时, 会放到basic最近的位子上, 具体分两种情况:
     * 1. first为空, 则放在first上 : first -> basic
     * 2. first不为空, 则放在离basic最近的位子上
     * first -> v1 -> v2 -> basic , 此时要插入 v3, 结果是: first -> v1 -> v2 -> v3 -> basic*/
    @Override
    public void addValve(Valve valve) {
    
        // Validate that we can add this Valve
        if (valve instanceof Contained)
            ((Contained) valve).setContainer(this.container);
    
        // Start the new component if necessary
        if (getState().isAvailable()) {
            if (valve instanceof Lifecycle) {
                try {
                    ((Lifecycle) valve).start();
                } catch (LifecycleException e) {
                    log.error(sm.getString("standardPipeline.valve.start"), e);
                }
            }
        }
    
        // Add this Valve to the set associated with this Pipeline
        if (first == null) {
            first = valve;
            valve.setNext(basic);
        }
        else {
            Valve current = first;
            while (current != null) {
                if (current.getNext() == basic) {
                    current.setNext(valve);
                    valve.setNext(basic);
                    break;
                }
                current = current.getNext();
            }
        }
    
        container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve);
    }

    初始状态下, first 是空的, 所有的阀门, 在新增的时候, 都是紧挨着basic的位置放的.

    getFirst()

    /**
     * 拿取优先级:(有头拿头, 没头拿尾)
     * 1. first存在, 则返回first
     * 2. first不存在, 则返回 basic
     * @return
     */
    @Override
    public Valve getFirst() {
        if (first != null) {
            return first;
        }
    
        return basic;
    }

    在获取的时候, 当 first 为空时, 直接获取 basic , 说明此时只有一个阀门

    当first 不为空时, 则会拿取 first , 然后需要在 调用阀门的时候, 进行传递.

    StandardEngineValve

    public final void invoke(Request request, Response response)
        throws IOException, ServletException {
    
        // Select the Host to be used for this Request
        Host host = request.getHost();
        if (host == null) {
            response.sendError
                (HttpServletResponse.SC_BAD_REQUEST,
                 sm.getString("standardEngine.noHost",
                              request.getServerName()));
            return;
        }
        if (request.isAsyncSupported()) {
            request.setAsyncSupported(host.getPipeline().isAsyncSupported());
        }
    
        // Ask this Host to process this request
        //调用 StandardHostValve#invoke() 方法
        host.getPipeline()   //-->StandardPipeline
                .getFirst()  //--> AccessLogValve --> ErrorReportValve --> StandardHostValve
                .invoke(request, response);
    
    }

    StandardEngineValve 作为管道的 basic(末位) , 肩负着调用子节点的管道方法. 这里启动了 StandardHostValve

    AccessLogValve 

    //org.apache.catalina.valves.AbstractAccessLogValve#invoke
    public void invoke(Request request, Response response) throws IOException,
            ServletException {
        if (tlsAttributeRequired) {
            // The log pattern uses TLS attributes. Ensure these are populated
            // before the request is processed because with NIO2 it is possible
            // for the connection to be closed (and the TLS info lost) before
            // the access log requests the TLS info. Requesting it now causes it
            // to be cached in the request.
            request.getAttribute(Globals.CERTIFICATES_ATTR);
        }
        if (cachedElements != null) {
            for (CachedElement element : cachedElements) {
                element.cache(request);
            }
        }
        getNext().invoke(request, response);
    }

    ErrorReportValve 

    public void invoke(Request request, Response response) throws IOException, ServletException {
    
        // Perform the request
        getNext().invoke(request, response);
    
        if (response.isCommitted()) {
            if (response.setErrorReported()) {
                // Error wasn't previously reported but we can't write an error
                // page because the response has already been committed. Attempt
                // to flush any data that is still to be written to the client.
                try {
                    response.flushBuffer();
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                }
                // Close immediately to signal to the client that something went
                // wrong
                response.getCoyoteResponse().action(ActionCode.CLOSE_NOW,
                        request.getAttribute(RequestDispatcher.ERROR_EXCEPTION));
            }
            return;
        }
    
        Throwable throwable = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
    
        // If an async request is in progress and is not going to end once this
        // container thread finishes, do not process any error page here.
        if (request.isAsync() && !request.isAsyncCompleting()) {
            return;
        }
    
        if (throwable != null && !response.isError()) {
            // Make sure that the necessary methods have been called on the
            // response. (It is possible a component may just have set the
            // Throwable. Tomcat won't do that but other components might.)
            // These are safe to call at this point as we know that the response
            // has not been committed.
            response.reset();
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    
        // One way or another, response.sendError() will have been called before
        // execution reaches this point and suspended the response. Need to
        // reverse that so this valve can write to the response.
        response.setSuspended(false);
    
        try {
            report(request, response, throwable);
        } catch (Throwable tt) {
            ExceptionUtils.handleThrowable(tt);
        }
    }

    这里是先进行了传递, 然后根据后面执行的结果, 再进行一些错误处理.

    StandardHostValve

    public final void invoke(Request request, Response response)
        throws IOException, ServletException {
    
        // Select the Context to be used for this Request.   context --> StandardContext
        Context context = request.getContext();
        if (context == null) {
            return;
        }
    
        if (request.isAsyncSupported()) {
            request.setAsyncSupported(context.getPipeline().isAsyncSupported());
        }
    
        boolean asyncAtStart = request.isAsync();
    
        try {
            context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
         //fireRequestInitEvent 最终会调用 Servlet 监听器 ServletRequestListener#requestInitialized()
            if (!asyncAtStart && !context.fireRequestInitEvent(request.getRequest())) {
                // Don't fire listeners during async processing (the listener
                // fired for the request that called startAsync()).
                // If a request init listener throws an exception, the request
                // is aborted.
                return;
            }
    
            // Ask this Context to process this request. Requests that are
            // already in error must have been routed here to check for
            // application defined error pages so DO NOT forward them to the the
            // application for processing.
            try {
                if (!response.isErrorReportRequired()) {
                    //StandardContextValve#invoke()
                    context.getPipeline()   //-->StandardPipeline
                            .getFirst()   //--> NonLoginAuthenticator --> StandardContextValve
                            .invoke(request, response);
                }
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                container.getLogger().error("Exception Processing " + request.getRequestURI(), t);
                // If a new error occurred while trying to report a previous
                // error allow the original error to be reported.
                if (!response.isErrorReportRequired()) {
                    request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
                    throwable(request, response, t);
                }
            }
    
            // Now that the request/response pair is back under container
            // control lift the suspension so that the error handling can
            // complete and/or the container can flush any remaining data
            response.setSuspended(false);
    
            Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
    
            // Protect against NPEs if the context was destroyed during a
            // long running request.
            if (!context.getState().isAvailable()) {
                return;
            }
    
            // Look for (and render if found) an application level error page
            if (response.isErrorReportRequired()) {
                // If an error has occurred that prevents further I/O, don't waste time
                // producing an error report that will never be read
                AtomicBoolean result = new AtomicBoolean(false);
                response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
                if (result.get()) {
                    if (t != null) {
                        throwable(request, response, t);
                    } else {
                        status(request, response);
                    }
                }
            }
    
            if (!request.isAsync() && !asyncAtStart) {
           //最终会调用 Servlet 监听器 ServletRequestListener#requestDestroyed() context.fireRequestDestroyEvent(request.getRequest()); } }
    finally { // Access a session (if present) to update last accessed time, based // on a strict interpretation of the specification if (ACCESS_SESSION) { request.getSession(false); } context.unbind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER); } }

    StandardHostValve 作为末位valve, 同样的, 肩负着启动子节点的管道.

    NonLoginAuthenticator

    public void invoke(Request request, Response response) throws IOException, ServletException {
    
        if (log.isDebugEnabled()) {
            log.debug("Security checking request " + request.getMethod() + " " +
                    request.getRequestURI());
        }
    
        // Have we got a cached authenticated Principal to record?
        if (cache) {
            Principal principal = request.getUserPrincipal();
            if (principal == null) {
                Session session = request.getSessionInternal(false);
                if (session != null) {
                    principal = session.getPrincipal();
                    if (principal != null) {
                        if (log.isDebugEnabled()) {
                            log.debug("We have cached auth type " + session.getAuthType() +
                                    " for principal " + principal);
                        }
                        request.setAuthType(session.getAuthType());
                        request.setUserPrincipal(principal);
                    }
                }
            }
        }
    
        boolean authRequired = isContinuationRequired(request);
    
        Realm realm = this.context.getRealm();
        // Is this request URI subject to a security constraint?
        SecurityConstraint[] constraints = realm.findSecurityConstraints(request, this.context);
    
        AuthConfigProvider jaspicProvider = getJaspicProvider();
        if (jaspicProvider != null) {
            authRequired = true;
        }
    
        if (constraints == null && !context.getPreemptiveAuthentication() && !authRequired) {
            if (log.isDebugEnabled()) {
                log.debug("Not subject to any constraint");
            }
            getNext().invoke(request, response);
            return;
        }
    
        // Make sure that constrained resources are not cached by web proxies
        // or browsers as caching can provide a security hole
        if (constraints != null && disableProxyCaching &&
                !"POST".equalsIgnoreCase(request.getMethod())) {
            if (securePagesWithPragma) {
                // Note: These can cause problems with downloading files with IE
                response.setHeader("Pragma", "No-cache");
                response.setHeader("Cache-Control", "no-cache");
            } else {
                response.setHeader("Cache-Control", "private");
            }
            response.setHeader("Expires", DATE_ONE);
        }
    
        if (constraints != null) {
            // Enforce any user data constraint for this security constraint
            if (log.isDebugEnabled()) {
                log.debug("Calling hasUserDataPermission()");
            }
            if (!realm.hasUserDataPermission(request, response, constraints)) {
                if (log.isDebugEnabled()) {
                    log.debug("Failed hasUserDataPermission() test");
                }
                /*
                 * ASSERT: Authenticator already set the appropriate HTTP status
                 * code, so we do not have to do anything special
                 */
                return;
            }
        }
    
        // Since authenticate modifies the response on failure,
        // we have to check for allow-from-all first.
        boolean hasAuthConstraint = false;
        if (constraints != null) {
            hasAuthConstraint = true;
            for (int i = 0; i < constraints.length && hasAuthConstraint; i++) {
                if (!constraints[i].getAuthConstraint()) {
                    hasAuthConstraint = false;
                } else if (!constraints[i].getAllRoles() &&
                        !constraints[i].getAuthenticatedUsers()) {
                    String[] roles = constraints[i].findAuthRoles();
                    if (roles == null || roles.length == 0) {
                        hasAuthConstraint = false;
                    }
                }
            }
        }
    
        if (!authRequired && hasAuthConstraint) {
            authRequired = true;
        }
    
        if (!authRequired && context.getPreemptiveAuthentication()) {
            authRequired =
                    request.getCoyoteRequest().getMimeHeaders().getValue("authorization") != null;
        }
    
        if (!authRequired && context.getPreemptiveAuthentication() &&
                HttpServletRequest.CLIENT_CERT_AUTH.equals(getAuthMethod())) {
            X509Certificate[] certs = getRequestCertificates(request);
            authRequired = certs != null && certs.length > 0;
        }
    
        JaspicState jaspicState = null;
    
        if ((authRequired || constraints != null) && allowCorsPreflightBypass(request)) {
            if (log.isDebugEnabled()) {
                log.debug("CORS Preflight request bypassing authentication");
            }
            getNext().invoke(request, response);
            return;
        }
    
        if (authRequired) {
            if (log.isDebugEnabled()) {
                log.debug("Calling authenticate()");
            }
    
            if (jaspicProvider != null) {
                jaspicState = getJaspicState(jaspicProvider, request, response, hasAuthConstraint);
                if (jaspicState == null) {
                    return;
                }
            }
    
            if (jaspicProvider == null && !doAuthenticate(request, response) ||
                    jaspicProvider != null &&
                            !authenticateJaspic(request, response, jaspicState, false)) {
                if (log.isDebugEnabled()) {
                    log.debug("Failed authenticate() test");
                }
                /*
                 * ASSERT: Authenticator already set the appropriate HTTP status
                 * code, so we do not have to do anything special
                 */
                return;
            }
    
        }
    
        if (constraints != null) {
            if (log.isDebugEnabled()) {
                log.debug("Calling accessControl()");
            }
            if (!realm.hasResourcePermission(request, response, constraints, this.context)) {
                if (log.isDebugEnabled()) {
                    log.debug("Failed accessControl() test");
                }
                /*
                 * ASSERT: AccessControl method has already set the appropriate
                 * HTTP status code, so we do not have to do anything special
                 */
                return;
            }
        }
    
        // Any and all specified constraints have been satisfied
        if (log.isDebugEnabled()) {
            log.debug("Successfully passed all security constraints");
        }
        getNext().invoke(request, response);
    
        if (jaspicProvider != null) {
            secureResponseJspic(request, response, jaspicState);
        }
    }

    这个阀门是进行tomcat身份验证的.

    StandardContextValve

    public final void invoke(Request request, Response response)
        throws IOException, ServletException {
    
        // Disallow any direct access to resources under WEB-INF or META-INF
        MessageBytes requestPathMB = request.getRequestPathMB();
        if ((requestPathMB.startsWithIgnoreCase("/META-INF/", 0))
                || (requestPathMB.equalsIgnoreCase("/META-INF"))
                || (requestPathMB.startsWithIgnoreCase("/WEB-INF/", 0))
                || (requestPathMB.equalsIgnoreCase("/WEB-INF"))) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
    
        // Select the Wrapper to be used for this Request
        Wrapper wrapper = request.getWrapper();
        if (wrapper == null || wrapper.isUnavailable()) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
    
        // Acknowledge the request
        try {
            response.sendAcknowledgement();
        } catch (IOException ioe) {
            container.getLogger().error(sm.getString(
                    "standardContextValve.acknowledgeException"), ioe);
            request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, ioe);
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            return;
        }
    
        if (request.isAsyncSupported()) {
            request.setAsyncSupported(wrapper.getPipeline().isAsyncSupported());
        }
        //StandardWrapperValve#invoke()
        wrapper.getPipeline()   //-->StandardPipeline
                .getFirst()   //-->StandardWrapperValve
                .invoke(request, response);
    }

    StandardWrapperValve

    public final void invoke(Request request, Response response)
        throws IOException, ServletException {
    
        // Initialize local variables we may need
        boolean unavailable = false;
        Throwable throwable = null;
        // This should be a Request attribute...
        long t1=System.currentTimeMillis();
        //增加请求次数 CAS乐观锁的方式
        requestCount.incrementAndGet();
        StandardWrapper wrapper = (StandardWrapper) getContainer();
        Servlet servlet = null;
        Context context = (Context) wrapper.getParent();
    
        // Check for the application being marked unavailable
        if (!context.getState().isAvailable()) {
            response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                           sm.getString("standardContext.isUnavailable"));
            unavailable = true;
        }
    
        // Check for the servlet being marked unavailable
        if (!unavailable && wrapper.isUnavailable()) {
            container.getLogger().info(sm.getString("standardWrapper.isUnavailable",
                    wrapper.getName()));
            long available = wrapper.getAvailable();
            if ((available > 0L) && (available < Long.MAX_VALUE)) {
                response.setDateHeader("Retry-After", available);
                response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                        sm.getString("standardWrapper.isUnavailable",
                                wrapper.getName()));
            } else if (available == Long.MAX_VALUE) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND,
                        sm.getString("standardWrapper.notFound",
                                wrapper.getName()));
            }
            unavailable = true;
        }
    
        // Allocate a servlet instance to process this request
        try {
            if (!unavailable) {
                servlet = wrapper.allocate();
            }
        }
        catch (UnavailableException e) {
            container.getLogger().error(
                    sm.getString("standardWrapper.allocateException",
                            wrapper.getName()), e);
            long available = wrapper.getAvailable();
            if ((available > 0L) && (available < Long.MAX_VALUE)) {
                response.setDateHeader("Retry-After", available);
                response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                           sm.getString("standardWrapper.isUnavailable",
                                        wrapper.getName()));
            } else if (available == Long.MAX_VALUE) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND,
                           sm.getString("standardWrapper.notFound",
                                        wrapper.getName()));
            }
        }
        catch (ServletException e) {
            container.getLogger().error(sm.getString("standardWrapper.allocateException",
                             wrapper.getName()), StandardWrapper.getRootCause(e));
            throwable = e;
            exception(request, response, e);
        }
        catch (Throwable e) {
            ExceptionUtils.handleThrowable(e);
            container.getLogger().error(sm.getString("standardWrapper.allocateException",
                             wrapper.getName()), e);
            throwable = e;
            exception(request, response, e);
            servlet = null;
        }
    
        MessageBytes requestPathMB = request.getRequestPathMB();
        DispatcherType dispatcherType = DispatcherType.REQUEST;
        if (request.getDispatcherType()==DispatcherType.ASYNC) dispatcherType = DispatcherType.ASYNC;
        request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,dispatcherType);
        request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, requestPathMB);
        // Create the filter chain for this request
        // 创建过滤器链
        ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
    
        // Call the filter chain for this request
        // NOTE: This also calls the servlet's service() method
        try {
            if ((servlet != null) && (filterChain != null)) {
                // Swallow output if needed
                if (context.getSwallowOutput()) {
                    try {
                        SystemLogHandler.startCapture();
                        if (request.isAsyncDispatching()) {
                            request.getAsyncContextInternal().doInternalDispatch();
                        } else {
                            //执行过滤链
                            filterChain.doFilter(request.getRequest(), response.getResponse());
                        }
                    } finally {
                        String log = SystemLogHandler.stopCapture();
                        if (log != null && log.length() > 0) {
                            context.getLogger().info(log);
                        }
                    }
                } else {
                    if (request.isAsyncDispatching()) {
                        request.getAsyncContextInternal().doInternalDispatch();
                    } else {
                //执行过滤器链方法, 最终会执行 Servlet 的 service 方法 filterChain.doFilter(request.getRequest(), response.getResponse()); } } } }
    catch (ClientAbortException | CloseNowException e) { if (container.getLogger().isDebugEnabled()) { container.getLogger().debug(sm.getString( "standardWrapper.serviceException", wrapper.getName(), context.getName()), e); } throwable = e; exception(request, response, e); } catch (IOException e) { container.getLogger().error(sm.getString( "standardWrapper.serviceException", wrapper.getName(), context.getName()), e); throwable = e; exception(request, response, e); } catch (UnavailableException e) { container.getLogger().error(sm.getString( "standardWrapper.serviceException", wrapper.getName(), context.getName()), e); // throwable = e; // exception(request, response, e); wrapper.unavailable(e); long available = wrapper.getAvailable(); if ((available > 0L) && (available < Long.MAX_VALUE)) { response.setDateHeader("Retry-After", available); response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, sm.getString("standardWrapper.isUnavailable", wrapper.getName())); } else if (available == Long.MAX_VALUE) { response.sendError(HttpServletResponse.SC_NOT_FOUND, sm.getString("standardWrapper.notFound", wrapper.getName())); } // Do not save exception in 'throwable', because we // do not want to do exception(request, response, e) processing } catch (ServletException e) { Throwable rootCause = StandardWrapper.getRootCause(e); if (!(rootCause instanceof ClientAbortException)) { container.getLogger().error(sm.getString( "standardWrapper.serviceExceptionRoot", wrapper.getName(), context.getName(), e.getMessage()), rootCause); } throwable = e; exception(request, response, e); } catch (Throwable e) { ExceptionUtils.handleThrowable(e); container.getLogger().error(sm.getString( "standardWrapper.serviceException", wrapper.getName(), context.getName()), e); throwable = e; exception(request, response, e); } finally { // Release the filter chain (if any) for this request if (filterChain != null) { //释放过滤链资源 filterChain.release(); } // Deallocate the allocated servlet instance try { if (servlet != null) { //会受到 Servlet 池例中 wrapper.deallocate(servlet); } } catch (Throwable e) { ExceptionUtils.handleThrowable(e); container.getLogger().error(sm.getString("standardWrapper.deallocateException", wrapper.getName()), e); if (throwable == null) { throwable = e; exception(request, response, e); } } // If this servlet has been marked permanently unavailable, // unload it and release this instance try { if ((servlet != null) && (wrapper.getAvailable() == Long.MAX_VALUE)) { wrapper.unload(); } } catch (Throwable e) { ExceptionUtils.handleThrowable(e); container.getLogger().error(sm.getString("standardWrapper.unloadException", wrapper.getName()), e); if (throwable == null) { exception(request, response, e); } } long t2=System.currentTimeMillis(); long time=t2-t1; processingTime += time; if( time > maxTime) maxTime=time; if( time < minTime) minTime=time; } }

    总结

  • 相关阅读:
    【002】有符号数据传递给无符号变量
    C++第二课 数据类型和常变量
    【001】冒泡排序
    iOS中为网站添加图标到主屏幕以及增加启动画面
    _stdcall,_cdecl区别
    解决表格里面使用text-overflow后依旧不能隐藏超出的文本
    windows7 64位下运行 regsvr32 注册ocx或者dll的方法
    在sqlite中使用索引
    ASP中 Request.Form中文乱码的解决方法
    html——标签基础
  • 原文地址:https://www.cnblogs.com/elvinle/p/13535950.html
Copyright © 2020-2023  润新知