diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java index b4e2e4fc6048..220279e98509 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2014 JetBrains s.r.o. + * Copyright 2000-2015 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,11 +21,15 @@ import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.util.SystemInfoRt; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.util.text.StringUtilRt; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.PathUtilRt; import com.intellij.util.UriUtil; import com.intellij.util.io.URLUtil; import com.intellij.util.net.NetUtils; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtf8Writer; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; @@ -34,6 +38,8 @@ import io.netty.handler.codec.http.*; import io.netty.handler.stream.ChunkedStream; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.builtInWebServer.ssi.SsiExternalResolver; +import org.jetbrains.builtInWebServer.ssi.SsiProcessor; import org.jetbrains.ide.HttpRequestHandler; import org.jetbrains.io.FileResponses; import org.jetbrains.io.Responses; @@ -43,6 +49,8 @@ import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; +import static org.jetbrains.io.Responses.addKeepAliveIfNeed; + public final class BuiltInWebServer extends HttpRequestHandler { static final Logger LOG = Logger.getInstance(BuiltInWebServer.class); @@ -59,6 +67,7 @@ public final class BuiltInWebServer extends HttpRequestHandler { for (VirtualFile child : children) { if (!child.isDirectory()) { String name = child.getName(); + //noinspection IfStatementWithIdenticalBranches if (name.equals(preferredName)) { return child; } @@ -81,7 +90,7 @@ public final class BuiltInWebServer extends HttpRequestHandler { @Override public boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) { - String host = HttpHeaders.getHost(request); + String host = request.headers().get(HttpHeaderNames.HOST); if (StringUtil.isEmpty(host)) { return false; } @@ -178,13 +187,23 @@ public final class BuiltInWebServer extends HttpRequestHandler { } static final class StaticFileHandler extends WebServerFileHandler { + private SsiProcessor ssiProcessor; + @Override public boolean process(@NotNull VirtualFile file, @NotNull CharSequence canonicalRequestPath, @NotNull Project project, @NotNull FullHttpRequest request, - @NotNull Channel channel) throws IOException { + @NotNull Channel channel, + boolean isCustomHost) throws IOException { if (file.isInLocalFileSystem()) { + CharSequence nameSequence = file.getNameSequence(); + //noinspection SpellCheckingInspection + if (StringUtilRt.endsWithIgnoreCase(nameSequence, ".shtml") || StringUtilRt.endsWithIgnoreCase(nameSequence, ".stm") || StringUtilRt.endsWithIgnoreCase(nameSequence, ".shtm")) { + processSsi(file, canonicalRequestPath, project, request, channel, isCustomHost); + return true; + } + File ioFile = VfsUtilCore.virtualToIoFile(file); if (hasAccess(ioFile)) { FileResponses.sendFile(request, channel, ioFile); @@ -199,9 +218,9 @@ public final class BuiltInWebServer extends HttpRequestHandler { return true; } - boolean keepAlive = Responses.addKeepAliveIfNeed(response, request); + boolean keepAlive = addKeepAliveIfNeed(response, request); if (request.method() != HttpMethod.HEAD) { - HttpHeaders.setContentLength(response, file.getLength()); + HttpHeaderUtil.setContentLength(response, file.getLength()); } channel.write(response); @@ -218,6 +237,56 @@ public final class BuiltInWebServer extends HttpRequestHandler { return true; } + private void processSsi(@NotNull VirtualFile file, + @NotNull CharSequence canonicalRequestPath, + @NotNull Project project, + @NotNull FullHttpRequest request, @NotNull Channel channel, boolean isCustomHost) throws IOException { + String path = PathUtilRt.getParentPath(canonicalRequestPath.toString()); + if (!isCustomHost) { + // remove project name - SSI resolves files only inside current project + path = path.substring(path.indexOf('/', 1) + 1); + } + + if (ssiProcessor == null) { + ssiProcessor = new SsiProcessor(false); + } + + ByteBuf buffer = channel.alloc().ioBuffer(); + boolean keepAlive; + boolean releaseBuffer = true; + try { + long lastModified = ssiProcessor.process(new SsiExternalResolver(project, request, path, file.getParent()), + VfsUtilCore.loadText(file), file.getTimeStamp(), new ByteBufUtf8Writer(buffer)); + + HttpResponse response = FileResponses.prepareSend(request, channel, lastModified, file.getPath()); + if (response == null) { + return; + } + + keepAlive = addKeepAliveIfNeed(response, request); + if (request.method() != HttpMethod.HEAD) { + HttpHeaderUtil.setContentLength(response, buffer.readableBytes()); + } + + channel.write(response); + + if (request.method() != HttpMethod.HEAD) { + releaseBuffer = false; + channel.write(buffer); + } + } + finally { + if (releaseBuffer) { + buffer.release(); + } + } + + ChannelFuture future = channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); + if (!keepAlive) { + future.addListener(ChannelFutureListener.CLOSE); + } + } + private static boolean hasAccess(File result) { // deny access to .htaccess files return !result.isDirectory() && result.canRead() && !(result.isHidden() || result.getName().startsWith(".ht")); diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.java index 9cef834f0052..61094173ba33 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.java +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.java @@ -90,7 +90,7 @@ final class DefaultWebServerPathHandler extends WebServerPathHandler { for (WebServerFileHandler fileHandler : WebServerFileHandler.EP_NAME.getExtensions()) { try { - if (fileHandler.process(result, canonicalRequestPath, project, request, channel)) { + if (fileHandler.process(result, canonicalRequestPath, project, request, channel, isCustomHost)) { return true; } } diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.java index 9035af6c52cc..3d846ca74d44 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.java +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2014 JetBrains s.r.o. + * Copyright 2000-2015 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,5 +31,6 @@ public abstract class WebServerFileHandler { @NotNull CharSequence canonicalRequestPath, @NotNull Project project, @NotNull FullHttpRequest request, - @NotNull Channel channel) throws IOException; + @NotNull Channel channel, + boolean isCustomHost) throws IOException; } \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/ExpressionParseTree.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/ExpressionParseTree.java new file mode 100644 index 000000000000..e11ad6e45136 --- /dev/null +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/ExpressionParseTree.java @@ -0,0 +1,448 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer.ssi; + +import java.text.ParseException; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * Represents a parsed expression. + * + * @author Paul Speed + */ +final class ExpressionParseTree { + /** + * Contains the current set of completed nodes. This is a workspace for the + * parser. + */ + private final LinkedList nodeStack = new LinkedList(); + /** + * Contains operator nodes that don't yet have values. This is a workspace + * for the parser. + */ + private final LinkedList oppStack = new LinkedList(); + /** + * The root node after the expression has been parsed. + */ + private Node root; + /** + * The SSIMediator to use when evaluating the expressions. + */ + private final SsiProcessingState mySsiProcessingState; + + /** + * Creates a new parse tree for the specified expression. + */ + public ExpressionParseTree(String expr, SsiProcessingState ssiProcessingState) + throws ParseException { + this.mySsiProcessingState = ssiProcessingState; + parseExpression(expr); + } + + + /** + * Evaluates the tree and returns true or false. The specified SSIMediator + * is used to resolve variable references. + */ + public boolean evaluateTree() { + return root.evaluate(); + } + + + /** + * Pushes a new operator onto the opp stack, resolving existing opps as + * needed. + */ + private void pushOpp(OppNode node) { + // If node is null then it's just a group marker + if (node == null) { + oppStack.add(0, node); + return; + } + while (true) { + if (oppStack.size() == 0) break; + OppNode top = oppStack.get(0); + // If the top is a spacer then don't pop + // anything + //noinspection ConstantConditions + if (top == null) break; + // If the top node has a lower precedence then + // let it stay + if (top.getPrecedence() < node.getPrecedence()) break; + // Remove the top node + oppStack.remove(0); + // Let it fill its branches + top.popValues(nodeStack); + // Stick it on the resolved node stack + nodeStack.add(0, top); + } + // Add the new node to the opp stack + oppStack.add(0, node); + } + + + /** + * Resolves all pending opp nodes on the stack until the next group marker + * is reached. + */ + private void resolveGroup() { + OppNode top; + while ((top = oppStack.remove(0)) != null) { + // Let it fill its branches + top.popValues(nodeStack); + // Stick it on the resolved node stack + nodeStack.add(0, top); + } + } + + + /** + * Parses the specified expression into a tree of parse nodes. + */ + private void parseExpression(String expr) throws ParseException { + StringNode currStringNode = null; + // We cheat a little and start an artificial + // group right away. It makes finishing easier. + pushOpp(null); + ExpressionTokenizer et = new ExpressionTokenizer(expr); + while (et.hasMoreTokens()) { + int token = et.nextToken(); + if (token != ExpressionTokenizer.TOKEN_STRING) { + currStringNode = null; + } + switch (token) { + case ExpressionTokenizer.TOKEN_STRING: + if (currStringNode == null) { + currStringNode = new StringNode(et.getTokenValue()); + nodeStack.add(0, currStringNode); + } + else { + // Add to the existing + currStringNode.value.append(" "); + currStringNode.value.append(et.getTokenValue()); + } + break; + case ExpressionTokenizer.TOKEN_AND: + pushOpp(new AndNode()); + break; + case ExpressionTokenizer.TOKEN_OR: + pushOpp(new OrNode()); + break; + case ExpressionTokenizer.TOKEN_NOT: + pushOpp(new NotNode()); + break; + case ExpressionTokenizer.TOKEN_EQ: + pushOpp(new EqualNode()); + break; + case ExpressionTokenizer.TOKEN_NOT_EQ: + pushOpp(new NotNode()); + // Sneak the regular node in. The NOT will + // be resolved when the next opp comes along. + oppStack.add(0, new EqualNode()); + break; + case ExpressionTokenizer.TOKEN_RBRACE: + // Closeout the current group + resolveGroup(); + break; + case ExpressionTokenizer.TOKEN_LBRACE: + // Push a group marker + pushOpp(null); + break; + case ExpressionTokenizer.TOKEN_GE: + pushOpp(new NotNode()); + // Similar strategy to NOT_EQ above, except this + // is NOT less than + oppStack.add(0, new LessThanNode()); + break; + case ExpressionTokenizer.TOKEN_LE: + pushOpp(new NotNode()); + // Similar strategy to NOT_EQ above, except this + // is NOT greater than + oppStack.add(0, new GreaterThanNode()); + break; + case ExpressionTokenizer.TOKEN_GT: + pushOpp(new GreaterThanNode()); + break; + case ExpressionTokenizer.TOKEN_LT: + pushOpp(new LessThanNode()); + break; + case ExpressionTokenizer.TOKEN_END: + break; + } + } + // Finish off the rest of the uopps + resolveGroup(); + if (nodeStack.size() == 0) { + throw new ParseException("No nodes created.", et.getIndex()); + } + if (nodeStack.size() > 1) { + throw new ParseException("Extra nodes created.", et.getIndex()); + } + if (oppStack.size() != 0) { + throw new ParseException("Unused opp nodes exist.", et.getIndex()); + } + root = nodeStack.get(0); + } + + /** + * A node in the expression parse tree. + */ + private abstract static class Node { + /** + * Return true if the node evaluates to true. + */ + public abstract boolean evaluate(); + } + + /** + * A node the represents a String value + */ + private class StringNode extends Node { + StringBuilder value; + String resolved = null; + + + public StringNode(String value) { + this.value = new StringBuilder(value); + } + + + /** + * Resolves any variable references and returns the value string. + */ + public String getValue() { + if (resolved == null) { + resolved = mySsiProcessingState.substituteVariables(value.toString()); + } + return resolved; + } + + + /** + * Returns true if the string is not empty. + */ + @Override + public boolean evaluate() { + return !(getValue().length() == 0); + } + + + @Override + public String toString() { + return value.toString(); + } + } + + private static final int PRECEDENCE_NOT = 5; + private static final int PRECEDENCE_COMPARE = 4; + private static final int PRECEDENCE_LOGICAL = 1; + + /** + * A node implementation that represents an operation. + */ + private abstract static class OppNode extends Node { + /** + * The left branch. + */ + Node left; + /** + * The right branch. + */ + Node right; + + + /** + * Returns a preference level suitable for comparison to other OppNode + * preference levels. + */ + public abstract int getPrecedence(); + + + /** + * Lets the node pop its own branch nodes off the front of the + * specified list. The default pulls two. + */ + public void popValues(List values) { + right = values.remove(0); + left = values.remove(0); + } + } + + private static final class NotNode extends OppNode { + @Override + public boolean evaluate() { + return !left.evaluate(); + } + + + @Override + public int getPrecedence() { + return PRECEDENCE_NOT; + } + + + /** + * Overridden to pop only one value. + */ + @Override + public void popValues(List values) { + left = values.remove(0); + } + + + @Override + public String toString() { + return left + " NOT"; + } + } + + private static final class AndNode extends OppNode { + @Override + public boolean evaluate() { + if (!left.evaluate()) // Short circuit + { + return false; + } + return right.evaluate(); + } + + + @Override + public int getPrecedence() { + return PRECEDENCE_LOGICAL; + } + + + @Override + public String toString() { + return left + " " + right + " AND"; + } + } + + private static final class OrNode extends OppNode { + @Override + public boolean evaluate() { + if (left.evaluate()) // Short circuit + { + return true; + } + return right.evaluate(); + } + + + @Override + public int getPrecedence() { + return PRECEDENCE_LOGICAL; + } + + + @Override + public String toString() { + return left + " " + right + " OR"; + } + } + + private abstract static class CompareNode extends OppNode { + protected int compareBranches() { + String val1 = ((StringNode)left).getValue(); + String val2 = ((StringNode)right).getValue(); + + int val2Len = val2.length(); + if (val2Len > 1 && val2.charAt(0) == '/' && + val2.charAt(val2Len - 1) == '/') { + // Treat as a regular expression + String expr = val2.substring(1, val2Len - 1); + try { + Pattern pattern = Pattern.compile(expr); + // Regular expressions will only ever be used with EqualNode + // so return zero for equal and non-zero for not equal + if (pattern.matcher(val1).find()) { + return 0; + } + else { + return -1; + } + } + catch (PatternSyntaxException e) { + SsiProcessor.LOG.warn("Invalid expression: " + expr, e); + return 0; + } + } + return val1.compareTo(val2); + } + } + + private static final class EqualNode extends CompareNode { + @Override + public boolean evaluate() { + return (compareBranches() == 0); + } + + + @Override + public int getPrecedence() { + return PRECEDENCE_COMPARE; + } + + + @Override + public String toString() { + return left + " " + right + " EQ"; + } + } + + private static final class GreaterThanNode extends CompareNode { + @Override + public boolean evaluate() { + return (compareBranches() > 0); + } + + + @Override + public int getPrecedence() { + return PRECEDENCE_COMPARE; + } + + + @Override + public String toString() { + return left + " " + right + " GT"; + } + } + + private static final class LessThanNode extends CompareNode { + @Override + public boolean evaluate() { + return (compareBranches() < 0); + } + + + @Override + public int getPrecedence() { + return PRECEDENCE_COMPARE; + } + + + @Override + public String toString() { + return left + " " + right + " LT"; + } + } +} \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/ExpressionTokenizer.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/ExpressionTokenizer.java new file mode 100644 index 000000000000..482cacf5dc99 --- /dev/null +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/ExpressionTokenizer.java @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer.ssi; + +/** + * Parses an expression string to return the individual tokens. This is + * patterned similar to the StreamTokenizer in the JDK but customized for SSI + * conditional expression parsing. + * + * @author Paul Speed + */ +public class ExpressionTokenizer { + public static final int TOKEN_STRING = 0; + public static final int TOKEN_AND = 1; + public static final int TOKEN_OR = 2; + public static final int TOKEN_NOT = 3; + public static final int TOKEN_EQ = 4; + public static final int TOKEN_NOT_EQ = 5; + public static final int TOKEN_RBRACE = 6; + public static final int TOKEN_LBRACE = 7; + public static final int TOKEN_GE = 8; + public static final int TOKEN_LE = 9; + public static final int TOKEN_GT = 10; + public static final int TOKEN_LT = 11; + public static final int TOKEN_END = 12; + private final char[] expr; + private String tokenVal = null; + private int index; + private final int length; + + /** + * Creates a new parser for the specified expression. + */ + public ExpressionTokenizer(String expr) { + this.expr = expr.trim().toCharArray(); + this.length = this.expr.length; + } + + /** + * Returns true if there are more tokens. + */ + public boolean hasMoreTokens() { + return index < length; + } + + /** + * Returns the current index for error reporting purposes. + */ + public int getIndex() { + return index; + } + + protected boolean isMetaChar(char c) { + return Character.isWhitespace(c) || c == '(' || c == ')' || c == '!' + || c == '<' || c == '>' || c == '|' || c == '&' || c == '='; + } + + + /** + * Returns the next token type and initializes any state variables + * accordingly. + */ + public int nextToken() { + // Skip any leading white space + while (index < length && Character.isWhitespace(expr[index])) { + index++; + } + // Clear the current token val + tokenVal = null; + if (index == length) return TOKEN_END; // End of string + int start = index; + char currentChar = expr[index]; + char nextChar = (char)0; + index++; + if (index < length) nextChar = expr[index]; + // Check for a known token start + switch (currentChar) { + case '(': + return TOKEN_LBRACE; + case ')': + return TOKEN_RBRACE; + case '=': + return TOKEN_EQ; + case '!': + if (nextChar == '=') { + index++; + return TOKEN_NOT_EQ; + } + return TOKEN_NOT; + case '|': + if (nextChar == '|') { + index++; + return TOKEN_OR; + } + break; + case '&': + if (nextChar == '&') { + index++; + return TOKEN_AND; + } + break; + case '>': + if (nextChar == '=') { + index++; + return TOKEN_GE; // Greater than or equal + } + return TOKEN_GT; // Greater than + case '<': + if (nextChar == '=') { + index++; + return TOKEN_LE; // Less than or equal + } + return TOKEN_LT; // Less than + default: + // Otherwise it's a string + break; + } + int end = index; + if (currentChar == '"' || currentChar == '\'') { + // It's a quoted string and the end is the next unescaped quote + char endChar = currentChar; + boolean escaped = false; + start++; + for (; index < length; index++) { + if (expr[index] == '\\' && !escaped) { + escaped = true; + continue; + } + if (expr[index] == endChar && !escaped) break; + escaped = false; + } + end = index; + index++; // Skip the end quote + } + else if (currentChar == '/') { + // It's a regular expression and the end is the next unescaped / + char endChar = currentChar; + boolean escaped = false; + for (; index < length; index++) { + if (expr[index] == '\\' && !escaped) { + escaped = true; + continue; + } + if (expr[index] == endChar && !escaped) break; + escaped = false; + } + end = ++index; + } + else { + // End is the next whitespace character + for (; index < length; index++) { + if (isMetaChar(expr[index])) break; + } + end = index; + } + // Extract the string from the array + this.tokenVal = new String(expr, start, end - start); + return TOKEN_STRING; + } + + /** + * Returns the String value of the token if it was type TOKEN_STRING. + * Otherwise null is returned. + */ + public String getTokenValue() { + return tokenVal; + } +} \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiCommand.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiCommand.java new file mode 100644 index 000000000000..448bbeafe653 --- /dev/null +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiCommand.java @@ -0,0 +1,25 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer.ssi; + +import io.netty.buffer.ByteBufUtf8Writer; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public interface SsiCommand { + long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer); +} \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiConditional.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiConditional.java new file mode 100644 index 000000000000..52f727cb5a38 --- /dev/null +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiConditional.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer.ssi; + +import io.netty.buffer.ByteBufUtf8Writer; +import org.jetbrains.annotations.NotNull; + +import java.text.ParseException; +import java.util.List; + +/** + * SSI command that handles all conditional directives. + * + * @author Paul Speed + * @author David Becker + */ +public class SsiConditional implements SsiCommand { + @SuppressWarnings("SpellCheckingInspection") + @Override + public long process(@NotNull SsiProcessingState ssiProcessingState, @NotNull String commandName, @NotNull List paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) { + // Assume anything using conditionals was modified by it + long lastModified = System.currentTimeMillis(); + // Retrieve the current state information + SsiProcessingState.SsiConditionalState state = ssiProcessingState.conditionalState; + if ("if".equalsIgnoreCase(commandName)) { + // Do nothing if we are nested in a false branch + // except count it + if (state.processConditionalCommandsOnly) { + state.nestingCount++; + return lastModified; + } + state.nestingCount = 0; + // Evaluate the expression + if (evaluateArguments(paramNames, paramValues, ssiProcessingState)) { + // No more branches can be taken for this if block + state.branchTaken = true; + } + else { + // Do not process this branch + state.processConditionalCommandsOnly = true; + state.branchTaken = false; + } + } + else if ("elif".equalsIgnoreCase(commandName)) { + // No need to even execute if we are nested in + // a false branch + if (state.nestingCount > 0) return lastModified; + // If a branch was already taken in this if block + // then disable output and return + if (state.branchTaken) { + state.processConditionalCommandsOnly = true; + return lastModified; + } + // Evaluate the expression + if (evaluateArguments(paramNames, paramValues, ssiProcessingState)) { + // Turn back on output and mark the branch + state.processConditionalCommandsOnly = false; + state.branchTaken = true; + } + else { + // Do not process this branch + state.processConditionalCommandsOnly = true; + state.branchTaken = false; + } + } + else if ("else".equalsIgnoreCase(commandName)) { + // No need to even execute if we are nested in + // a false branch + if (state.nestingCount > 0) return lastModified; + // If we've already taken another branch then + // disable output otherwise enable it. + state.processConditionalCommandsOnly = state.branchTaken; + // And in any case, it's safe to say a branch + // has been taken. + state.branchTaken = true; + } + else if ("endif".equalsIgnoreCase(commandName)) { + // If we are nested inside a false branch then pop out + // one level on the nesting count + if (state.nestingCount > 0) { + state.nestingCount--; + return lastModified; + } + // Turn output back on + state.processConditionalCommandsOnly = false; + // Reset the branch status for any outer if blocks, + // since clearly we took a branch to have gotten here + // in the first place. + state.branchTaken = true; + } + else { + throw new SsiStopProcessingException(); + } + return lastModified; + } + + /** + * Retrieves the expression from the specified arguments and performs the necessary evaluation steps. + */ + private static boolean evaluateArguments(@NotNull List names, @NotNull String[] values, @NotNull SsiProcessingState ssiProcessingState) { + String expression = "expr".equalsIgnoreCase(names.get(0)) ? values[0] : null; + if (expression == null) { + throw new SsiStopProcessingException(); + } + try { + return new ExpressionParseTree(expression, ssiProcessingState).evaluateTree(); + } + catch (ParseException e) { + throw new SsiStopProcessingException(); + } + } +} \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiExternalResolver.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiExternalResolver.java new file mode 100644 index 000000000000..2f4b4d9a34f8 --- /dev/null +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiExternalResolver.java @@ -0,0 +1,97 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer.ssi; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.VirtualFile; +import gnu.trove.THashMap; +import io.netty.handler.codec.http.HttpRequest; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.builtInWebServer.WebServerPathToFileManager; + +import java.util.Collection; +import java.util.Map; + +public final class SsiExternalResolver { + private final String[] VARIABLE_NAMES = {"AUTH_TYPE", "CONTENT_LENGTH", + "CONTENT_TYPE", "DOCUMENT_NAME", "DOCUMENT_URI", + "GATEWAY_INTERFACE", "HTTP_ACCEPT", "HTTP_ACCEPT_ENCODING", + "HTTP_ACCEPT_LANGUAGE", "HTTP_CONNECTION", "HTTP_HOST", + "HTTP_REFERER", "HTTP_USER_AGENT", "PATH_INFO", "PATH_TRANSLATED", + "QUERY_STRING", "QUERY_STRING_UNESCAPED", "REMOTE_ADDR", + "REMOTE_HOST", "REMOTE_PORT", "REMOTE_USER", "REQUEST_METHOD", + "REQUEST_URI", "SCRIPT_FILENAME", "SCRIPT_NAME", "SERVER_ADDR", + "SERVER_NAME", "SERVER_PORT", "SERVER_PROTOCOL", "SERVER_SOFTWARE", + "UNIQUE_ID"}; + + private final Project project; + private final HttpRequest request; + + private Map variables = new THashMap(); + private final String parentPath; + @NotNull private final VirtualFile parentFile; + + public SsiExternalResolver(@NotNull Project project, + @NotNull HttpRequest request, + @NotNull String parentPath, + @NotNull VirtualFile parentFile) { + this.project = project; + this.request = request; + this.parentPath = parentPath; + this.parentFile = parentFile; + } + + public void addVariableNames(@NotNull Collection variableNames) { + for (String variableName : VARIABLE_NAMES) { + String variableValue = getVariableValue(variableName); + if (variableValue != null) { + variableNames.add(variableName); + } + } + } + + public void setVariableValue(@NotNull String name, String value) { + variables.put(name, value); + } + + public String getVariableValue(@NotNull String name) { + String value = variables.get(name); + return value == null ? request.headers().get(name) : value; + } + + @Nullable + public VirtualFile findFile(@NotNull String originalPath, boolean virtual) { + String path = FileUtil.toCanonicalPath(originalPath, '/'); + if (!virtual) { + return parentFile.findFileByRelativePath(path); + } + + path = path.charAt(0) == '/' ? path : (parentPath + '/' + path); + return WebServerPathToFileManager.getInstance(project).get(path); + } + + public long getFileLastModified(String path, boolean virtual) { + VirtualFile file = findFile(path, virtual); + return file == null ? 0 : file.getTimeStamp(); + } + + public long getFileSize(@NotNull String path, boolean virtual) { + VirtualFile file = findFile(path, virtual); + return file == null ? -1 : file.getLength(); + } +} \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiFsize.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiFsize.java new file mode 100644 index 000000000000..1d436117f783 --- /dev/null +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiFsize.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer.ssi; + +import io.netty.buffer.ByteBufUtf8Writer; +import org.jetbrains.annotations.NotNull; + +import java.text.DecimalFormat; +import java.util.List; + +/** + * Implements the Server-side #fsize command + * + * @author Bip Thelin + * @author Paul Speed + * @author Dan Sandberg + * @author David Becker + */ +final class SsiFsize implements SsiCommand { + private static final int ONE_KILOBYTE = 1024; + private static final int ONE_MEGABYTE = 1024 * 1024; + + @Override + public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) { + long lastModified = 0; + String configErrMsg = state.configErrorMessage; + for (int i = 0; i < paramNames.size(); i++) { + String paramName = paramNames.get(i); + String paramValue = paramValues[i]; + String substitutedValue = state.substituteVariables(paramValue); + if (paramName.equalsIgnoreCase("file") || paramName.equalsIgnoreCase("virtual")) { + boolean virtual = paramName.equalsIgnoreCase("virtual"); + lastModified = state.ssiExternalResolver.getFileLastModified(substitutedValue, virtual); + writer.write(formatSize(state.ssiExternalResolver.getFileSize(substitutedValue, virtual), state.configSizeFmt)); + } + else { + SsiProcessor.LOG.info("#fsize--Invalid attribute: " + paramName); + writer.write(configErrMsg); + } + } + return lastModified; + } + + // We try to mimic Apache here, as we do everywhere + // All the 'magic' numbers are from the util_script.c Apache source file. + private static String formatSize(long size, @NotNull String format) { + if (format.equalsIgnoreCase("bytes")) { + return new DecimalFormat("#,##0").format(size); + } + + String result; + if (size == 0) { + result = "0k"; + } + else if (size < ONE_KILOBYTE) { + result = "1k"; + } + else if (size < ONE_MEGABYTE) { + result = Long.toString((size + 512) / ONE_KILOBYTE) + "k"; + } + else if (size < 99 * ONE_MEGABYTE) { + result = new DecimalFormat("0.0M").format(size / (double)ONE_MEGABYTE); + } + else { + result = Long.toString((size + (529 * ONE_KILOBYTE)) / ONE_MEGABYTE) + "M"; + } + + int charsToAdd = 5 - result.length(); + if (charsToAdd < 0) { + throw new IllegalArgumentException("Num chars can't be negative"); + } + + if (charsToAdd == 0) { + return result; + } + + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < charsToAdd; i++) { + buf.append(' '); + } + return buf.append(result).toString(); + } +} \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiProcessingState.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiProcessingState.java new file mode 100644 index 000000000000..0d9821692908 --- /dev/null +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiProcessingState.java @@ -0,0 +1,202 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer.ssi; + +import com.google.common.escape.Escaper; +import com.google.common.html.HtmlEscapers; +import com.google.common.net.PercentEscaper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +public class SsiProcessingState { + protected static final String DEFAULT_CONFIG_ERR_MSG = "[an error occurred while processing this directive]"; + protected static final String DEFAULT_CONFIG_TIME_FMT = "%A, %d-%b-%Y %T %Z"; + protected static final String DEFAULT_CONFIG_SIZE_FMT = "abbrev"; + // encode only the same characters that apache does + protected static final Escaper urlEscaper = new PercentEscaper(",:-_.*/!~'()", false); + protected String configErrorMessage = DEFAULT_CONFIG_ERR_MSG; + protected String configTimeFmt = DEFAULT_CONFIG_TIME_FMT; + protected String configSizeFmt = DEFAULT_CONFIG_SIZE_FMT; + protected final SsiExternalResolver ssiExternalResolver; + protected final long lastModifiedDate; + protected Strftime strftime; + protected final SsiConditionalState conditionalState = new SsiConditionalState(); + + private boolean alreadySet; + + public SsiProcessingState(@NotNull SsiExternalResolver ssiExternalResolver, long lastModifiedDate) { + this.ssiExternalResolver = ssiExternalResolver; + this.lastModifiedDate = lastModifiedDate; + setConfigTimeFormat(DEFAULT_CONFIG_TIME_FMT, true); + } + + static class SsiConditionalState { + /** + * Set to true if the current conditional has already been completed, i.e.: + * a branch was taken. + */ + boolean branchTaken = false; + /** + * Counts the number of nested false branches. + */ + int nestingCount = 0; + /** + * Set to true if only conditional commands ( if, elif, else, endif ) + * should be processed. + */ + boolean processConditionalCommandsOnly = false; + } + + public void setConfigTimeFormat(String configTimeFmt, boolean fromConstructor) { + this.configTimeFmt = configTimeFmt; + this.strftime = new Strftime(configTimeFmt, Locale.US); + setDateVariables(fromConstructor); + } + + public String getVariableValue(String variableName) { + return getVariableValue(variableName, "none"); + } + + public String getVariableValue(@NotNull String variableName, String encoding) { + String variableValue = ssiExternalResolver.getVariableValue(variableName); + return variableValue == null ? null : encode(variableValue, encoding); + } + + /** + * Applies variable substitution to the specified String and returns the + * new resolved string. + */ + @SuppressWarnings("AssignmentToForLoopParameter") + public String substituteVariables(String val) { + // If it has no references or HTML entities then no work + // need to be done + if (val.indexOf('$') < 0 && val.indexOf('&') < 0) { + return val; + } + + val = val.replace("<", "<"); + val = val.replace(">", ">"); + val = val.replace(""", "\""); + val = val.replace("&", "&"); + + StringBuilder sb = new StringBuilder(val); + int charStart = sb.indexOf("&#"); + while (charStart > -1) { + int charEnd = sb.indexOf(";", charStart); + if (charEnd > -1) { + char c = (char)Integer.parseInt( + sb.substring(charStart + 2, charEnd)); + sb.delete(charStart, charEnd + 1); + sb.insert(charStart, c); + charStart = sb.indexOf("&#"); + } + else { + break; + } + } + + for (int i = 0; i < sb.length(); ) { + // Find the next $ + for (; i < sb.length(); i++) { + if (sb.charAt(i) == '$') { + i++; + break; + } + } + if (i == sb.length()) break; + // Check to see if the $ is escaped + if (i > 1 && sb.charAt(i - 2) == '\\') { + sb.deleteCharAt(i - 2); + i--; + continue; + } + int nameStart = i; + int start = i - 1; + int end; + int nameEnd; + char endChar = ' '; + // Check for {} wrapped var + if (sb.charAt(i) == '{') { + nameStart++; + endChar = '}'; + } + // Find the end of the var reference + for (; i < sb.length(); i++) { + if (sb.charAt(i) == endChar) break; + } + end = i; + nameEnd = end; + if (endChar == '}') end++; + // We should now have enough to extract the var name + String varName = sb.substring(nameStart, nameEnd); + String value = getVariableValue(varName); + if (value == null) value = ""; + // Replace the var name with its value + sb.replace(start, end, value); + // Start searching for the next $ after the value + // that was just substituted. + i = start + value.length(); + } + return sb.toString(); + } + + protected void setDateVariables(boolean fromConstructor) { + if (fromConstructor && alreadySet) { + return; + } + + alreadySet = true; + Date date = new Date(); + TimeZone timeZone = TimeZone.getTimeZone("GMT"); + ssiExternalResolver.setVariableValue("DATE_GMT", formatDate(date, timeZone)); + ssiExternalResolver.setVariableValue("DATE_LOCAL", formatDate(date, null)); + ssiExternalResolver.setVariableValue("LAST_MODIFIED", formatDate(new Date(lastModifiedDate), null)); + } + + @NotNull + protected String formatDate(@NotNull Date date, @Nullable TimeZone timeZone) { + if (timeZone == null) { + return strftime.format(date); + } + else { + TimeZone oldTimeZone = strftime.getTimeZone(); + strftime.setTimeZone(timeZone); + String retVal = strftime.format(date); + strftime.setTimeZone(oldTimeZone); + return retVal; + } + } + + @NotNull + protected String encode(@NotNull String value, @NotNull String encoding) { + if (encoding.equalsIgnoreCase("url")) { + return urlEscaper.escape(value); + } + else if (encoding.equalsIgnoreCase("none")) { + return value; + } + else if (encoding.equalsIgnoreCase("entity")) { + return HtmlEscapers.htmlEscaper().escape(value); + } + else { + throw new IllegalArgumentException("Unknown encoding: " + encoding); + } + } +} \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiProcessor.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiProcessor.java new file mode 100644 index 000000000000..42f19475c802 --- /dev/null +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiProcessor.java @@ -0,0 +1,440 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer.ssi; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.SmartList; +import gnu.trove.THashMap; +import io.netty.buffer.ByteBufUtf8Writer; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +public class SsiProcessor { + static final Logger LOG = Logger.getInstance(SsiProcessor.class); + + protected static final String COMMAND_START = ""; + + protected final Map commands = new THashMap(); + + @SuppressWarnings("SpellCheckingInspection") + public SsiProcessor(boolean allowExec) { + commands.put("config", new SsiCommand() { + @Override + public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) { + for (int i = 0; i < paramNames.size(); i++) { + String paramName = paramNames.get(i); + String paramValue = paramValues[i]; + String substitutedValue = state.substituteVariables(paramValue); + if (paramName.equalsIgnoreCase("errmsg")) { + state.configErrorMessage = substitutedValue; + } + else if (paramName.equalsIgnoreCase("sizefmt")) { + state.configSizeFmt = substitutedValue; + } + else if (paramName.equalsIgnoreCase("timefmt")) { + state.setConfigTimeFormat(substitutedValue, false); + } + else { + LOG.info("#config--Invalid attribute: " + paramName); + // We need to fetch this value each time, since it may change during the loop + writer.write(state.configErrorMessage); + } + } + return 0; + } + }); + commands.put("echo", new SsiCommand() { + @Override + public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) { + String encoding = "entity"; + String originalValue = null; + String errorMessage = state.configErrorMessage; + for (int i = 0; i < paramNames.size(); i++) { + String paramName = paramNames.get(i); + String paramValue = paramValues[i]; + if (paramName.equalsIgnoreCase("var")) { + originalValue = paramValue; + } + else if (paramName.equalsIgnoreCase("encoding")) { + if (paramValue.equalsIgnoreCase("url") || paramValue.equalsIgnoreCase("entity") || paramValue.equalsIgnoreCase("none")) { + encoding = paramValue; + } + else { + LOG.info("#echo--Invalid encoding: " + paramValue); + writer.write(errorMessage); + } + } + else { + LOG.info("#echo--Invalid attribute: " + paramName); + writer.write(errorMessage); + } + } + assert originalValue != null; + String variableValue = state.getVariableValue(originalValue, encoding); + writer.write(variableValue == null ? "(none)" : variableValue); + return System.currentTimeMillis(); + } + }); + //noinspection StatementWithEmptyBody + if (allowExec) { + // commands.put("exec", new SsiExec()); + } + commands.put("include", new SsiCommand() { + @Override + public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) { + long lastModified = 0; + String configErrorMessage = state.configErrorMessage; + for (int i = 0; i < paramNames.size(); i++) { + String paramName = paramNames.get(i); + if (paramName.equalsIgnoreCase("file") || paramName.equalsIgnoreCase("virtual")) { + String substitutedValue = state.substituteVariables(paramValues[i]); + try { + boolean virtual = paramName.equalsIgnoreCase("virtual"); + lastModified = state.ssiExternalResolver.getFileLastModified(substitutedValue, virtual); + VirtualFile file = state.ssiExternalResolver.findFile(substitutedValue, virtual); + if (file == null) { + LOG.warn("#include-- Couldn't find file: " + substitutedValue); + return 0; + } + + InputStream in = file.getInputStream(); + try { + writer.write(in, (int)file.getLength()); + } + finally { + in.close(); + } + } + catch (IOException e) { + LOG.warn("#include--Couldn't include file: " + substitutedValue, e); + writer.write(configErrorMessage); + } + } + else { + LOG.info("#include--Invalid attribute: " + paramName); + writer.write(configErrorMessage); + } + } + return lastModified; + } + }); + commands.put("flastmod", new SsiCommand() { + @Override + public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) { + long lastModified = 0; + String configErrMsg = state.configErrorMessage; + for (int i = 0; i < paramNames.size(); i++) { + String paramName = paramNames.get(i); + String paramValue = paramValues[i]; + String substitutedValue = state.substituteVariables(paramValue); + if (paramName.equalsIgnoreCase("file") || paramName.equalsIgnoreCase("virtual")) { + boolean virtual = paramName.equalsIgnoreCase("virtual"); + lastModified = state.ssiExternalResolver.getFileLastModified(substitutedValue, virtual); + Strftime strftime = new Strftime(state.configTimeFmt, Locale.US); + writer.write(strftime.format(new Date(lastModified))); + } + else { + LOG.info("#flastmod--Invalid attribute: " + paramName); + writer.write(configErrMsg); + } + } + return lastModified; + } + }); + commands.put("fsize", new SsiFsize()); + commands.put("printenv", new SsiCommand() { + @Override + public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) { + long lastModified = 0; + // any arguments should produce an error + if (paramNames.isEmpty()) { + Set variableNames = new LinkedHashSet(); + //These built-in variables are supplied by the mediator ( if not over-written by the user ) and always exist + variableNames.add("DATE_GMT"); + variableNames.add("DATE_LOCAL"); + variableNames.add("LAST_MODIFIED"); + state.ssiExternalResolver.addVariableNames(variableNames); + for (String variableName : variableNames) { + String variableValue = state.getVariableValue(variableName); + // This shouldn't happen, since all the variable names must have values + if (variableValue == null) { + variableValue = "(none)"; + } + writer.write(variableName); + writer.write('='); + writer.write(variableValue); + writer.write('\n'); + lastModified = System.currentTimeMillis(); + } + } + else { + writer.write(state.configErrorMessage); + } + return lastModified; + } + }); + commands.put("set", new SsiCommand() { + @Override + public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) { + long lastModified = 0; + String errorMessage = state.configErrorMessage; + String variableName = null; + for (int i = 0; i < paramNames.size(); i++) { + String paramName = paramNames.get(i); + String paramValue = paramValues[i]; + if (paramName.equalsIgnoreCase("var")) { + variableName = paramValue; + } + else if (paramName.equalsIgnoreCase("value")) { + if (variableName != null) { + String substitutedValue = state.substituteVariables(paramValue); + state.ssiExternalResolver.setVariableValue(variableName, substitutedValue); + lastModified = System.currentTimeMillis(); + } + else { + LOG.info("#set--no variable specified"); + writer.write(errorMessage); + throw new SsiStopProcessingException(); + } + } + else { + LOG.info("#set--Invalid attribute: " + paramName); + writer.write(errorMessage); + throw new SsiStopProcessingException(); + } + } + return lastModified; + } + }); + + SsiConditional ssiConditional = new SsiConditional(); + commands.put("if", ssiConditional); + commands.put("elif", ssiConditional); + commands.put("endif", ssiConditional); + commands.put("else", ssiConditional); + } + + /** + * @return the most current modified date resulting from any SSI commands + */ + public long process(@NotNull SsiExternalResolver ssiExternalResolver, @NotNull String fileContents, long lastModifiedDate, @NotNull ByteBufUtf8Writer writer) { + SsiProcessingState ssiProcessingState = new SsiProcessingState(ssiExternalResolver, lastModifiedDate); + int index = 0; + boolean inside = false; + StringBuilder command = new StringBuilder(); + try { + while (index < fileContents.length()) { + char c = fileContents.charAt(index); + if (inside) { + if (c == COMMAND_END.charAt(0) && charCmp(fileContents, index, COMMAND_END)) { + inside = false; + index += COMMAND_END.length(); + String commandName = parseCommand(command); + if (LOG.isDebugEnabled()) { + LOG.debug("SSIProcessor.process -- processing command: " + commandName); + } + List paramNames = parseParamNames(command, commandName.length()); + String[] paramValues = parseParamValues(command, commandName.length(), paramNames.size()); + // We need to fetch this value each time, since it may change during the loop + String configErrMsg = ssiProcessingState.configErrorMessage; + SsiCommand ssiCommand = commands.get(commandName.toLowerCase(Locale.ENGLISH)); + String errorMessage = null; + if (ssiCommand == null) { + errorMessage = "Unknown command: " + commandName; + } + else if (paramValues == null) { + errorMessage = "Error parsing directive parameters."; + } + else if (paramNames.size() != paramValues.length) { + errorMessage = "Parameter names count does not match parameter values count on command: " + commandName; + } + else { + // don't process the command if we are processing conditional commands only and the command is not conditional + if (!ssiProcessingState.conditionalState.processConditionalCommandsOnly || ssiCommand instanceof SsiConditional) { + long newLastModified = ssiCommand.process(ssiProcessingState, commandName, paramNames, paramValues, writer); + if (newLastModified > lastModifiedDate) { + lastModifiedDate = newLastModified; + } + } + } + if (errorMessage != null) { + LOG.warn(errorMessage); + writer.write(configErrMsg); + } + } + else { + command.append(c); + index++; + } + } + else if (c == COMMAND_START.charAt(0) && charCmp(fileContents, index, COMMAND_START)) { + inside = true; + index += COMMAND_START.length(); + command.setLength(0); + } + else { + if (!ssiProcessingState.conditionalState.processConditionalCommandsOnly) { + writer.write(c); + } + index++; + } + } + } + catch (SsiStopProcessingException e) { + //If we are here, then we have already stopped processing, so all is good + } + return lastModifiedDate; + } + + @NotNull + protected List parseParamNames(@NotNull StringBuilder command, int start) { + int bIdx = start; + List values = new SmartList(); + boolean inside = false; + StringBuilder builder = new StringBuilder(); + while (bIdx < command.length()) { + if (inside) { + while (bIdx < command.length() && command.charAt(bIdx) != '=') { + builder.append(command.charAt(bIdx)); + bIdx++; + } + + values.add(builder.toString()); + builder.setLength(0); + inside = false; + int quotes = 0; + boolean escaped = false; + for (; bIdx < command.length() && quotes != 2; bIdx++) { + char c = command.charAt(bIdx); + // Need to skip escaped characters + if (c == '\\' && !escaped) { + escaped = true; + continue; + } + if (c == '"' && !escaped) { + quotes++; + } + escaped = false; + } + } + else { + while (bIdx < command.length() && isSpace(command.charAt(bIdx))) { + bIdx++; + } + if (bIdx >= command.length()) { + break; + } + inside = true; + } + } + return values; + } + + @SuppressWarnings("AssignmentToForLoopParameter") + protected String[] parseParamValues(@NotNull StringBuilder command, int start, int count) { + int valueIndex = 0; + boolean inside = false; + String[] values = new String[count]; + StringBuilder builder = new StringBuilder(); + char endQuote = 0; + for (int bIdx = start; bIdx < command.length(); bIdx++) { + if (!inside) { + while (bIdx < command.length() && !isQuote(command.charAt(bIdx))) { + bIdx++; + } + if (bIdx >= command.length()) { + break; + } + inside = true; + endQuote = command.charAt(bIdx); + } + else { + boolean escaped = false; + for (; bIdx < command.length(); bIdx++) { + char c = command.charAt(bIdx); + // Check for escapes + if (c == '\\' && !escaped) { + escaped = true; + continue; + } + // If we reach the other " then stop + if (c == endQuote && !escaped) { + break; + } + // Since parsing of attributes and var + // substitution is done in separate places, + // we need to leave escape in the string + if (c == '$' && escaped) { + builder.append('\\'); + } + escaped = false; + builder.append(c); + } + // If we hit the end without seeing a quote + // the signal an error + if (bIdx == command.length()) { + return null; + } + values[valueIndex++] = builder.toString(); + builder.setLength(0); + inside = false; + } + } + return values; + } + + @NotNull + private String parseCommand(@NotNull StringBuilder instruction) { + int firstLetter = -1; + int lastLetter = -1; + for (int i = 0; i < instruction.length(); i++) { + char c = instruction.charAt(i); + if (Character.isLetter(c)) { + if (firstLetter == -1) { + firstLetter = i; + } + lastLetter = i; + } + else if (isSpace(c)) { + if (lastLetter > -1) { + break; + } + } + else { + break; + } + } + return firstLetter == -1 ? "" : instruction.substring(firstLetter, lastLetter + 1); + } + + protected boolean charCmp(String buf, int index, String command) { + return buf.regionMatches(index, command, 0, command.length()); + } + + + protected boolean isSpace(char c) { + return c == ' ' || c == '\n' || c == '\t' || c == '\r'; + } + + protected boolean isQuote(char c) { + return c == '\'' || c == '\"' || c == '`'; + } +} \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiStopProcessingException.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiStopProcessingException.java new file mode 100644 index 000000000000..fd0289681f6b --- /dev/null +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiStopProcessingException.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer.ssi; + + +/** + * Exception used to tell SSIProcessor that it should stop processing SSI + * commands. This is used to mimic the Apache behavior in #set with invalid + * attributes. + * + * @author Paul Speed + * @author Dan Sandberg + */ +public class SsiStopProcessingException extends RuntimeException { +} \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/Strftime.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/Strftime.java new file mode 100644 index 000000000000..80edb6b10bcd --- /dev/null +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/Strftime.java @@ -0,0 +1,255 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer.ssi; + +import gnu.trove.THashMap; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +/** + * Converts dates to strings using the same format specifiers as strftime + *

+ * Note: This does not mimic strftime perfectly. Certain strftime commands, + * are not supported, and will convert as if they were literals. + *

+ * Certain complicated commands, like those dealing with the week of the year + * probably don't have exactly the same behavior as strftime. + *

+ * These limitations are due to use SimpleDateTime. If the conversion was done + * manually, all these limitations could be eliminated. + *

+ * The interface looks like a subset of DateFormat. Maybe someday someone will make this class + * extend DateFormat. + * + * @author Bip Thelin + * @author Dan Sandberg + */ +@SuppressWarnings("SpellCheckingInspection") +public class Strftime { + protected static final Map translate = new THashMap(); + protected final SimpleDateFormat simpleDateFormat; + + static { + translate.put("a", "EEE"); + translate.put("A", "EEEE"); + translate.put("b", "MMM"); + translate.put("B", "MMMM"); + translate.put("c", "EEE MMM d HH:mm:ss yyyy"); + + //There's no way to specify the century in SimpleDateFormat. We don't want to hard-code + //20 since this could be wrong for the pre-2000 files. + //translate.put("C", "20"); + translate.put("d", "dd"); + translate.put("D", "MM/dd/yy"); + translate.put("e", "dd"); //will show as '03' instead of ' 3' + translate.put("F", "yyyy-MM-dd"); + translate.put("g", "yy"); + translate.put("G", "yyyy"); + translate.put("H", "HH"); + translate.put("h", "MMM"); + translate.put("I", "hh"); + translate.put("j", "DDD"); + translate.put("k", "HH"); //will show as '07' instead of ' 7' + translate.put("l", "hh"); //will show as '07' instead of ' 7' + translate.put("m", "MM"); + translate.put("M", "mm"); + translate.put("n", "\n"); + translate.put("p", "a"); + translate.put("P", "a"); //will show as pm instead of PM + translate.put("r", "hh:mm:ss a"); + translate.put("R", "HH:mm"); + //There's no way to specify this with SimpleDateFormat + //translate.put("s","seconds since epoch"); + translate.put("S", "ss"); + translate.put("t", "\t"); + translate.put("T", "HH:mm:ss"); + //There's no way to specify this with SimpleDateFormat + //translate.put("u","day of week ( 1-7 )"); + + //There's no way to specify this with SimpleDateFormat + //translate.put("U","week in year with first Sunday as first day..."); + + translate.put("V", "ww"); //I'm not sure this is always exactly the same + + //There's no way to specify this with SimpleDateFormat + //translate.put("W","week in year with first Monday as first day..."); + + //There's no way to specify this with SimpleDateFormat + //translate.put("w","E"); + translate.put("X", "HH:mm:ss"); + translate.put("x", "MM/dd/yy"); + translate.put("y", "yy"); + translate.put("Y", "yyyy"); + translate.put("Z", "z"); + translate.put("z", "Z"); + translate.put("%", "%"); + } + + /** + * Create an instance of this date formatting class + * + * @param origFormat the strftime-style formatting string + * @param locale the locale to use for locale-specific conversions + */ + public Strftime(String origFormat, Locale locale) { + String convertedFormat = convertDateFormat(origFormat); + simpleDateFormat = new SimpleDateFormat(convertedFormat, locale); + } + + /** + * Format the date according to the strftime-style string given in the constructor. + * + * @param date the date to format + * @return the formatted date + */ + public String format(Date date) { + return simpleDateFormat.format(date); + } + + /** + * Get the timezone used for formatting conversions + * + * @return the timezone + */ + public TimeZone getTimeZone() { + return simpleDateFormat.getTimeZone(); + } + + /** + * Change the timezone used to format dates + * + * @see SimpleDateFormat#setTimeZone + */ + public void setTimeZone(TimeZone timeZone) { + simpleDateFormat.setTimeZone(timeZone); + } + + /** + * Search the provided pattern and get the C standard + * Date/Time formatting rules and convert them to the + * Java equivalent. + * + * @param pattern The pattern to search + * @return The modified pattern + */ + protected String convertDateFormat(String pattern) { + boolean inside = false; + boolean mark = false; + boolean modifiedCommand = false; + + StringBuilder buf = new StringBuilder(); + + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + + if (c == '%' && !mark) { + mark = true; + } + else { + if (mark) { + if (modifiedCommand) { + //don't do anything--we just wanted to skip a char + modifiedCommand = false; + mark = false; + } + else { + inside = translateCommand(buf, pattern, i, inside); + //It's a modifier code + if (c == 'O' || c == 'E') { + modifiedCommand = true; + } + else { + mark = false; + } + } + } + else { + if (!inside && c != ' ') { + //We start a literal, which we need to quote + buf.append("'"); + inside = true; + } + + buf.append(c); + } + } + } + + if (buf.length() > 0) { + char lastChar = buf.charAt(buf.length() - 1); + + if (lastChar != '\'' && inside) { + buf.append('\''); + } + } + return buf.toString(); + } + + protected String quote(String str, boolean insideQuotes) { + String retVal = str; + if (!insideQuotes) { + retVal = '\'' + retVal + '\''; + } + return retVal; + } + + /** + * Try to get the Java Date/Time formatting associated with + * the C standard provided. + * + * @param buf The buffer + * @param pattern The date/time pattern + * @param index The char index + * @param oldInside Flag value + * @return True if new is inside buffer + */ + protected boolean translateCommand(StringBuilder buf, String pattern, int index, boolean oldInside) { + char firstChar = pattern.charAt(index); + boolean newInside = oldInside; + + //O and E are modifiers, they mean to present an alternative representation of the next char + //we just handle the next char as if the O or E wasn't there + if (firstChar == 'O' || firstChar == 'E') { + if (index + 1 < pattern.length()) { + newInside = translateCommand(buf, pattern, index + 1, oldInside); + } + else { + buf.append(quote("%" + firstChar, oldInside)); + } + } + else { + String command = translate.get(String.valueOf(firstChar)); + //If we don't find a format, treat it as a literal--That's what apache does + if (command == null) { + buf.append(quote("%" + firstChar, oldInside)); + } + else { + //If we were inside quotes, close the quotes + if (oldInside) { + buf.append('\''); + } + buf.append(command); + newInside = false; + } + } + return newInside; + } +} diff --git a/platform/built-in-server/src/org/jetbrains/io/FileResponses.java b/platform/built-in-server/src/org/jetbrains/io/FileResponses.java index a1c16f1e0460..c9323e02b7be 100644 --- a/platform/built-in-server/src/org/jetbrains/io/FileResponses.java +++ b/platform/built-in-server/src/org/jetbrains/io/FileResponses.java @@ -96,7 +96,7 @@ public class FileResponses { try { long fileLength = raf.length(); if (request.method() != HttpMethod.HEAD) { - HttpHeaders.setContentLength(response, fileLength); + HttpHeaderUtil.setContentLength(response, fileLength); } channel.write(response); diff --git a/platform/platform-impl/src/io/netty/buffer/ByteBufUtf8Writer.java b/platform/platform-impl/src/io/netty/buffer/ByteBufUtf8Writer.java index da6479b35fce..788291746672 100644 --- a/platform/platform-impl/src/io/netty/buffer/ByteBufUtf8Writer.java +++ b/platform/platform-impl/src/io/netty/buffer/ByteBufUtf8Writer.java @@ -19,6 +19,7 @@ import com.intellij.util.text.CharArrayCharSequence; import org.jetbrains.annotations.NotNull; import java.io.IOException; +import java.io.InputStream; import java.io.Writer; public final class ByteBufUtf8Writer extends Writer { @@ -28,6 +29,10 @@ public final class ByteBufUtf8Writer extends Writer { this.buffer = buffer; } + public void write(@NotNull InputStream inputStream, int length) throws IOException { + buffer.writeBytes(inputStream, length); + } + @Override public void write(int c) { AbstractByteBuf buffer = ByteBufUtilEx.getBuf(this.buffer); @@ -53,7 +58,7 @@ public final class ByteBufUtf8Writer extends Writer { } @Override - public void write(String str) throws IOException { + public void write(String str) { ByteBufUtilEx.writeUtf8(buffer, str); }