Статьи

Взлом Jasper для получения объектной модели JSP-страницы

Для выполнения некоторых проверок и статистического анализа моих JSP мне понадобилась DOM-подобная иерархическая модель элементов, содержащихся в них. Но синтаксический анализ страниц JSP не является тривиальным и его лучше оставить для инструмента, который выделяется в нем — JSP-компилятор Jasper, используемый Tomcat, Jetty, GlassFish и, вероятно, также всеми остальными.

Существует простой способ настроить его так, чтобы он получал любой вывод, необходимый для преобразования JSP в любую нужную форму, включая объектную модель страницы:

  1. Определите подкласс Node.Visitor для обработки узлов (тегов и т. Д.) JSP.
  2. Написать простой подкласс Compiler, переопределяя его generateJava () для вызова посетителя.
  3. Подкласс исполнителя-компилятора JspC переопределяет его метод getCompilerClassName () для возврата вашего класса компилятора

Давайте посмотрим код.

Реализация

1. Пользовательский посетитель

Visitor вызывается компилятором для обработки объектной модели дерева проанализированного JSP. Эта реализация просто печатает информацию об интересующем подмножестве узлов на странице с отступом, чтобы сделать их вложение понятным.

01
02
03
04
05
06
07
08
09
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
package org.apache.jasper.compiler;
 
import java.util.LinkedList;
import org.apache.jasper.JasperException;
import org.apache.jasper.compiler.Node.CustomTag;
import org.apache.jasper.compiler.Node.ELExpression;
import org.apache.jasper.compiler.Node.IncludeDirective;
import org.apache.jasper.compiler.Node.Visitor;
import org.xml.sax.Attributes;
 
public class JsfElCheckingVisitor extends Visitor {
 
    private String indent = "";
 
    @Override
    public void visit(ELExpression n) throws JasperException {
        logEntry("ELExpression", n, "EL: " + n.getEL());
        super.visit(n);
    }
 
    @Override
    public void visit(IncludeDirective n) throws JasperException {
        logEntry("IncludeDirective", n, toString(n.getAttributes()));
        super.visit(n);
    }
 
    @Override
    public void visit(CustomTag n) throws JasperException {
        logEntry("CustomTag", n, "Class: " + n.getTagHandlerClass().getName() + ", attrs: "
                + toString(n.getAttributes()));
 
        doVisit(n);
 
        indent += " ";
        visitBody(n);
        indent = indent.substring(0, indent.length() - 1);
    }
 
    private String toString(Attributes attributes) {
        if (attributes == null || attributes.getLength() == 0) return "";
        LinkedList<String> details = new LinkedList<String>();
 
        for (int i = 0; i < attributes.getLength(); i++) {
            details.add(attributes.getQName(i) + "=" + attributes.getValue(i));
        }
 
        return details.toString();
    }
 
    private void logEntry(String what, Node n, String details) {
        System.out.println(indent + n.getQName() + " at line:"
                + n.getStart().getLineNumber() + ": " + details);
    }
 
}

Заметки:

  • Посетитель должен быть в пакете org.apache.jasper.compiler, поскольку необходимый класс org.apache.jasper.compiler.Node является закрытым для пакета.
  • Метод visitBody запускает обработку вложенных узлов
  • Есть и другие методы, которые я мог бы переопределить (и метод catch-all doVisit), но я выбрал только те, которые мне интересны
  • Атрибуты узла имеют тип… саксофон. Атрибуты , которые содержат имена атрибутов и значения в виде строк
    • attribute.getType (i) обычно является CDATA
  • Структура Node содержит информацию о родительском узле, имени тега, классе обработчика тега, соответствующей строке исходного файла и имени исходного файла, а также другую полезную информацию.
  • CustomTag , вероятно, наиболее интересный тип узла, например, все теги JSF относятся к этому типу.

Пример вывода (для страницы JSF)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
jsp:directive.include at line:5: [file=includes/stdjsp.jsp]
jsp:directive.include at line:6: [file=includes/ssoinclude.jsp]
f:verbatim at line:14: Class: com.sun.faces.taglib.jsf_core.VerbatimTag, attrs:
htm:div at line:62: Class: com.exadel.htmLib.tags.DivTag, attrs: [style=width:100%;]
 h:form at line:64: Class: com.sun.faces.taglib.html_basic.FormTag, attrs: [id=inputForm]
  htm:table at line:66: Class: com.exadel.htmLib.tags.TableTag, attrs: [cellpadding=0, width=100%, border=0, styleClass=clear box_main]
   htm:tr at line:71: Class: com.exadel.htmLib.tags.TrTag, attrs:
    htm:td at line:72: Class: com.exadel.htmLib.tags.TdTag, attrs:
    f:subview at line:73: Class: com.sun.faces.taglib.jsf_core.SubviewTag, attrs: [id=cars]
      jsp:directive.include at line:74: [file=/includes/cars.jsp]
      h:panelGroup at line:8: Class: com.sun.faces.taglib.html_basic.PanelGroupTag, attrs: [rendered=#{bookingHandler.flowersAvailable}]
...
   htm:tr at line:87: Class: com.exadel.htmLib.tags.TrTag, attrs: [style=height:5px]
    htm:td at line:87: Class: com.exadel.htmLib.tags.TdTag, attrs:

(Я не печатаю «закрывающие теги», поскольку ясно, что тег заканчивается, когда появляется другой узел с таким же или меньшим отступом или заканчивается вывод.)

2. Подкласс компилятора

Важной частью является generateJava, который я только что скопировал, удалил из него некоторый код и добавил вызов моего посетителя. Так что на самом деле только 3 строки в списке ниже являются новыми (6, 56, 70)

01
02
03
04
05
06
07
08
09
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
66
67
68
69
70
71
72
73
public class OnlyReadingJspPseudoCompiler extends Compiler {
 
    /** We're never compiling .java to .class. */
    @Override protected void generateClass(String[] smap) throws FileNotFoundException,
            JasperException, Exception {
        return;
    }
 
    /** Copied from {@link Compiler#generateJava()} and adjusted */
    @Override protected String[] generateJava() throws Exception {
 
        // Setup page info area
        pageInfo = new PageInfo(new BeanRepository(ctxt.getClassLoader(),
                errDispatcher), ctxt.getJspFile());
 
        // JH: Skipped processing of jsp-property-group in web.xml for the current page
 
        if (ctxt.isTagFile()) {
            try {
                double libraryVersion = Double.parseDouble(ctxt.getTagInfo()
                        .getTagLibrary().getRequiredVersion());
                if (libraryVersion < 2.0) {
                    pageInfo.setIsELIgnored("true", null, errDispatcher, true);
                }
                if (libraryVersion < 2.1) {
                    pageInfo.setDeferredSyntaxAllowedAsLiteral("true", null,
                            errDispatcher, true);
                }
            } catch (NumberFormatException ex) {
                errDispatcher.jspError(ex);
            }
        }
 
        ctxt.checkOutputDir();
 
        try {
            // Parse the file
            ParserController parserCtl = new ParserController(ctxt, this);
 
            // Pass 1 - the directives
            Node.Nodes directives =
                parserCtl.parseDirectives(ctxt.getJspFile());
            Validator.validateDirectives(this, directives);
 
            // Pass 2 - the whole translation unit
            pageNodes = parserCtl.parse(ctxt.getJspFile());
 
            // Validate and process attributes - don't re-validate the
            // directives we validated in pass 1
            /**
             * JH: The code above has been copied from Compiler#generateJava() with some
             * omissions and with using our own Visitor.
             * The code that used to follow was just deleted.
             * Note: The JSP's name is in ctxt.getJspFile()
             */
            pageNodes.visit(new JsfElCheckingVisitor());
 
        } finally {}
 
        return null;
    }
 
    /**
     * The parent's implementation, in our case, checks whether the target file
     * exists and returns true if it doesn't. However it is expensive so
     * we skip it by returning true directly.
     * @see org.apache.jasper.JspCompilationContext#getServletJavaFileName()
     */
    @Override public boolean isOutDated(boolean checkClass) {
        return true;
    }
 
}

Заметки:

  • Я удалил довольно много кода, неважного для меня, из генерации Java; для анализа другого типа, чем я предполагаю, часть этого кода могла бы быть полезной, поэтому посмотрите на оригинальный класс Compiler и решите сами.
  • На самом деле меня не волнуют EL JSP, поэтому можно было бы оптимизировать компилятор так, чтобы ему потребовался только один проход.

3. Компилятор-исполнитель

Трудно использовать компилятор напрямую, потому что он зависит от множества сложных настроек и объектов. Таким образом, проще всего повторно использовать задачу Ant JspC, что дает дополнительное преимущество поиска JSP для обработки. Как уже упоминалось, ключевым моментом является переопределение getCompilerClassName для возврата класса моего компилятора (строка 8)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.apache.jasper.JspC;
 
/** Extends JspC to use the compiler of our choice; Jasper version 6.0.29. */
public class JspCParsingToNodesOnly extends JspC {
 
    /** Overriden to return the class of ours (default = null => JdtCompiler) */
    @Override public String getCompilerClassName() {
        return OnlyReadingJspPseudoCompiler.class.getName();
    }
 
    public static void main(String[] args) {
        JspCParsingToNodesOnly jspc = new JspCParsingToNodesOnly();
 
        jspc.setUriroot("web"); // where to search for JSPs
        //jspc.setVerbose(1);     // 0 = false, 1 = true
        jspc.setJspFiles("helloJSFpage.jsp"); // leave unset to process all; comma-separated
 
        try {
            jspc.execute();
        } catch (JasperException e) {
            throw new RuntimeException(e);
        }
    }
}

Заметки:

  • JspC обычно находит все файлы в указанном Uriroot, но вы можете сказать ему, что он должен игнорировать все, кроме некоторых выбранных, передав их имена через запятую в setJspFiles.

Зависимости компиляции

В форме твоего плюща:

1
2
3
<dependency name="jasper" org="org.apache.tomcat" rev="6.0.29">
<dependency name="jasper-jdt" org="org.apache.tomcat" rev="6.0.29">
<dependency name="ant" org="org.apache.ant" rev="1.8.2">

Лицензия

Весь код здесь напрямую получен из Jasper и, следовательно, подпадает под ту же лицензию, то есть лицензию Apache, версия 2.0 .

Вывод

Jasper на самом деле не был разработан для расширения и модульности, что подтверждается тем фактом, что критически важный класс Node является закрытым пакетом, а его API настолько сложен, что повторное использование только его части очень сложно. К счастью, задача Ant JspC делает ее пригодной для использования вне контейнера сервлета, предоставляя некоторые «поддельные» объекты, и есть способ настроить ее под наши нужды с минимальными затратами, хотя это было нелегко понять. Мне пришлось применить некоторые хитрые уловки, а именно использовать вещи из закрытого пакета и переопределить метод, который не предназначен для переопределения ( generateJava ), но он работает и обеспечивает очень ценный вывод, который позволяет делать все, что вы захотите делать с JSP.