This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/10.1.x by this push:
new ecfff720b2 Switch lock tokens to UUID as recommended by RFC 4918
ecfff720b2 is described below
commit ecfff720b24c2af003f12306355dede56c12bb2e
Author: remm <[email protected]>
AuthorDate: Thu Oct 31 10:27:39 2024 +0100
Switch lock tokens to UUID as recommended by RFC 4918
Remove secret init parameter.
Remove some int constants, replaced with an enum for the type of
PROPFIND performed.
Process LOCK request body only if present, to avoid relying on an IOE
(if a body is present and bad, return 400 instead).
---
.../apache/catalina/servlets/WebdavServlet.java | 148 +++++++++------------
.../catalina/servlets/TestWebdavServlet.java | 34 ++---
webapps/docs/changelog.xml | 5 +
3 files changed, 85 insertions(+), 102 deletions(-)
diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java
b/java/org/apache/catalina/servlets/WebdavServlet.java
index 0ba428ca45..748687eff4 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -25,7 +25,6 @@ import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
@@ -38,6 +37,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
+import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -59,12 +59,10 @@ import org.apache.catalina.util.DOMWriter;
import org.apache.catalina.util.XMLWriter;
import org.apache.tomcat.PeriodicEventListener;
import org.apache.tomcat.util.IntrospectionUtils;
-import org.apache.tomcat.util.buf.HexUtils;
import org.apache.tomcat.util.http.ConcurrentDateFormat;
import org.apache.tomcat.util.http.FastHttpDateFormat;
import org.apache.tomcat.util.http.RequestUtil;
import org.apache.tomcat.util.http.WebdavIfHeader;
-import org.apache.tomcat.util.security.ConcurrentMessageDigest;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -175,36 +173,6 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
private static final String METHOD_UNLOCK = "UNLOCK";
- /**
- * PROPFIND - Specify a property mask.
- */
- private static final int FIND_BY_PROPERTY = 0;
-
-
- /**
- * PROPFIND - Display all properties.
- */
- private static final int FIND_ALL_PROP = 1;
-
-
- /**
- * PROPFIND - Return property names.
- */
- private static final int FIND_PROPERTY_NAMES = 2;
-
-
- /**
- * Create a new lock.
- */
- private static final int LOCK_CREATION = 0;
-
-
- /**
- * Refresh lock.
- */
- private static final int LOCK_REFRESH = 1;
-
-
/**
* Default lock timeout value.
*/
@@ -217,6 +185,12 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
private static final int MAX_TIMEOUT = 604800;
+ /**
+ * Default maximum depth.
+ */
+ private static final int MAX_DEPTH = 3;
+
+
/**
* Default namespace.
*/
@@ -224,11 +198,11 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
/**
- * Supported locks.
+ * Pre generated raw XML for supported locks.
*/
- protected static final String SUPPORTED_LOCKS = "<D:lockentry>" +
"<D:lockscope><D:exclusive/></D:lockscope>" +
- "<D:locktype><D:write/></D:locktype>" + "</D:lockentry>" +
"<D:lockentry>" +
- "<D:lockscope><D:shared/></D:lockscope>" +
"<D:locktype><D:write/></D:locktype>" + "</D:lockentry>";
+ protected static final String SUPPORTED_LOCKS =
+ "\n
<D:lockentry><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>\n"
+
+ "
<D:lockentry><D:lockscope><D:shared/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>\n";
/**
* Simple date format for the creation date ISO representation (partial).
@@ -237,6 +211,11 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
new ConcurrentDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US,
TimeZone.getTimeZone("GMT"));
+ /**
+ * Lock scheme used.
+ */
+ protected static final String LOCK_SCHEME = "urn:uuid:";
+
// ----------------------------------------------------- Instance Variables
/**
@@ -252,15 +231,9 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
/**
- * Secret information used to generate reasonably secure lock ids.
+ * Default depth in spec is infinite.
*/
- private String secret = "catalina";
-
-
- /**
- * Default depth in spec is infinite. Limit depth to 3 by default as
infinite depth makes operations very expensive.
- */
- private int maxDepth = 3;
+ private int maxDepth = MAX_DEPTH;
/**
@@ -300,10 +273,6 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
}
}
- if (getServletConfig().getInitParameter("secret") != null) {
- secret = getServletConfig().getInitParameter("secret");
- }
-
if (getServletConfig().getInitParameter("maxDepth") != null) {
maxDepth =
Integer.parseInt(getServletConfig().getInitParameter("maxDepth"));
}
@@ -506,9 +475,19 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
}
}
+ /**
+ * Type of PROPFIND request.
+ */
+ enum PropfindType {
+ FIND_BY_PROPERTY, FIND_ALL_PROP, FIND_PROPERTY_NAMES
+ }
+
+
+ /**
+ * Type of property update in a PROPPATCH.
+ */
enum PropertyUpdateType {
- SET,
- REMOVE
+ SET, REMOVE
}
@@ -643,7 +622,7 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
} else {
if ((parentPath != currentPath &&
parentLock.depth > 0) || parentPath == currentPath) {
if (parentLock.isExclusive()) {
- lockTokens.add("opaquelocktoken:" +
parentLock.token);
+ lockTokens.add(LOCK_SCHEME +
parentLock.token);
} else {
for (String token :
parentLock.sharedTokens) {
if (sharedLocks.get(token) ==
null) {
@@ -658,7 +637,7 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
if (sharedLock != null) {
if ((parentPath != currentPath
&& sharedLock.depth > 0) ||
parentPath ==
currentPath) {
-
lockTokens.add("opaquelocktoken:" + token);
+ lockTokens.add(LOCK_SCHEME
+ token);
}
}
}
@@ -814,7 +793,7 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
// Propfind depth
int depth = maxDepth;
// Propfind type
- int type = -1;
+ PropfindType type = null;
String depthStr = req.getHeader("Depth");
@@ -855,12 +834,12 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
case Node.ELEMENT_NODE:
String nodeName = getDAVNode(currentNode);
if ("prop".equals(nodeName)) {
- if (type >= 0) {
+ if (type != null) {
// Another was already defined
resp.sendError(WebdavStatus.SC_BAD_REQUEST);
return;
}
- type = FIND_BY_PROPERTY;
+ type = PropfindType.FIND_BY_PROPERTY;
NodeList propChildList =
currentNode.getChildNodes();
for (int j = 0; j < propChildList.getLength();
j++) {
Node currentNode2 = propChildList.item(j);
@@ -874,20 +853,20 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
}
}
if ("propname".equals(nodeName)) {
- if (type >= 0) {
+ if (type != null) {
// Another was already defined
resp.sendError(WebdavStatus.SC_BAD_REQUEST);
return;
}
- type = FIND_PROPERTY_NAMES;
+ type = PropfindType.FIND_PROPERTY_NAMES;
}
if ("allprop".equals(nodeName)) {
- if (type >= 0) {
+ if (type != null) {
// Another was already defined
resp.sendError(WebdavStatus.SC_BAD_REQUEST);
return;
}
- type = FIND_ALL_PROP;
+ type = PropfindType.FIND_ALL_PROP;
}
break;
}
@@ -897,13 +876,13 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
resp.sendError(WebdavStatus.SC_BAD_REQUEST);
return;
}
- if (type == -1) {
+ if (type == null) {
// Nothing meaningful in the propfind element
resp.sendError(WebdavStatus.SC_BAD_REQUEST);
return;
}
} else {
- type = FIND_ALL_PROP;
+ type = PropfindType.FIND_ALL_PROP;
}
WebResource resource = resources.getResource(path);
@@ -1376,24 +1355,28 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
}
lock.expiresAt = System.currentTimeMillis() + (lockDuration * 1000);
- int lockRequestType = LOCK_CREATION;
+ boolean lockCreation = false;
Node lockInfoNode = null;
- DocumentBuilder documentBuilder = getDocumentBuilder();
+ if (req.getContentLengthLong() > 0 ||
"chunked".equalsIgnoreCase(req.getHeader("Transfer-Encoding"))) {
+ DocumentBuilder documentBuilder = getDocumentBuilder();
- try {
- Document document = documentBuilder.parse(new
InputSource(req.getInputStream()));
+ try {
+ Document document = documentBuilder.parse(new
InputSource(req.getInputStream()));
- // Get the root element of the document
- Element rootElement = document.getDocumentElement();
- if (!"lockinfo".equals(getDAVNode(rootElement))) {
+ // Get the root element of the document
+ Element rootElement = document.getDocumentElement();
+ if (!"lockinfo".equals(getDAVNode(rootElement))) {
+ resp.sendError(WebdavStatus.SC_BAD_REQUEST);
+ return;
+ }
+ lockInfoNode = rootElement;
+ lockCreation = true;
+ } catch (IOException | SAXException e) {
resp.sendError(WebdavStatus.SC_BAD_REQUEST);
return;
}
- lockInfoNode = rootElement;
- } catch (IOException | SAXException e) {
- lockRequestType = LOCK_REFRESH;
}
if (lockInfoNode != null) {
@@ -1510,7 +1493,7 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
lock.path = path;
- if (lockRequestType == LOCK_CREATION) {
+ if (lockCreation) {
// Check if the resource or a parent is already locked
String parentPath = path;
@@ -1533,12 +1516,7 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
} while (true);
// Generating lock id
- String lockTokenStr = req.getServletPath() + "-" + lock.type + "-"
+ lock.scope + "-" +
- req.getUserPrincipal() + "-" + lock.depth + "-" +
lock.owner + "-" + lock.token + "-" +
- lock.expiresAt + "-" + System.currentTimeMillis() + "-" +
secret;
- String lockToken = HexUtils
-
.toHexString(ConcurrentMessageDigest.digestMD5(lockTokenStr.getBytes(StandardCharsets.ISO_8859_1)));
- lock.token = lockToken;
+ lock.token = UUID.randomUUID().toString();
if (resource.isDirectory() && lock.depth == maxDepth) {
@@ -1632,16 +1610,16 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
sharedLock.depth = maxDepth;
resourceLocks.put(path, sharedLock);
}
- sharedLock.sharedTokens.add(lockToken);
- sharedLocks.put(lockToken, lock);
+ sharedLock.sharedTokens.add(lock.token);
+ sharedLocks.put(lock.token, lock);
}
// Add the Lock-Token header as by RFC 2518 8.10.1
- resp.addHeader("Lock-Token", "<opaquelocktoken:" + lockToken +
">");
+ resp.addHeader("Lock-Token", "<" + LOCK_SCHEME + lock.token + ">");
}
- if (lockRequestType == LOCK_REFRESH) {
+ if (!lockCreation) {
String ifHeader = req.getHeader("If");
if (ifHeader == null) {
@@ -2485,7 +2463,7 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
}
- private void propfindResource(XMLWriter generatedXML, String rewrittenUrl,
String path, int propFindType,
+ private void propfindResource(XMLWriter generatedXML, String rewrittenUrl,
String path, PropfindType propFindType,
List<Node> properties, boolean isFile, long created, long
lastModified, long contentLength,
String contentType, String eTag) {
@@ -2861,7 +2839,7 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
generatedXML.writeElement("D", "locktoken", XMLWriter.OPENING);
generatedXML.writeElement("D", "href", XMLWriter.OPENING);
- generatedXML.writeText("opaquelocktoken:" + token);
+ generatedXML.writeText(LOCK_SCHEME + token);
generatedXML.writeElement("D", "href", XMLWriter.CLOSING);
generatedXML.writeElement("D", "locktoken", XMLWriter.CLOSING);
diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java
b/test/org/apache/catalina/servlets/TestWebdavServlet.java
index 526ee9c6e8..af51e99ffa 100644
--- a/test/org/apache/catalina/servlets/TestWebdavServlet.java
+++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java
@@ -281,7 +281,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.connect();
client.processRequest(true);
Assert.assertEquals(HttpServletResponse.SC_CREATED,
client.getStatusCode());
-
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertTrue(client.getResponseBody().contains("urn:uuid:"));
client.setRequest(new String[] { "PROPFIND / HTTP/1.1" +
SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
@@ -290,7 +290,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.connect();
client.processRequest(true);
Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS,
client.getStatusCode());
-
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertTrue(client.getResponseBody().contains("urn:uuid:"));
client.setRequest(new String[] { "PROPPATCH /file1.txt HTTP/1.1" +
SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
@@ -422,7 +422,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.connect();
client.processRequest(true);
Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
-
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertTrue(client.getResponseBody().contains("urn:uuid:"));
String lockToken = null;
for (String header : client.getResponseHeaders()) {
if (header.startsWith("Lock-Token: ")) {
@@ -490,7 +490,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.connect();
client.processRequest(true);
Assert.assertEquals(HttpServletResponse.SC_CREATED,
client.getStatusCode());
-
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertTrue(client.getResponseBody().contains("urn:uuid:"));
String lockTokenFile = null;
for (String header : client.getResponseHeaders()) {
if (header.startsWith("Lock-Token: ")) {
@@ -559,7 +559,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.connect();
client.processRequest(true);
Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
-
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertTrue(client.getResponseBody().contains("urn:uuid:"));
lockToken = null;
for (String header : client.getResponseHeaders()) {
if (header.startsWith("Lock-Token: ")) {
@@ -756,7 +756,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.connect();
client.processRequest(true);
Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
-
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertTrue(client.getResponseBody().contains("urn:uuid:"));
String lockToken = null;
for (String header : client.getResponseHeaders()) {
if (header.startsWith("Lock-Token: ")) {
@@ -795,7 +795,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.connect();
client.processRequest(true);
Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
-
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertTrue(client.getResponseBody().contains("urn:uuid:"));
String lockToken2 = null;
for (String header : client.getResponseHeaders()) {
if (header.startsWith("Lock-Token: ")) {
@@ -813,7 +813,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.connect();
client.processRequest(true);
Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
-
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertTrue(client.getResponseBody().contains("urn:uuid:"));
String lockToken3 = null;
for (String header : client.getResponseHeaders()) {
if (header.startsWith("Lock-Token: ")) {
@@ -830,7 +830,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.connect();
client.processRequest(true);
Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS,
client.getStatusCode());
-
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertTrue(client.getResponseBody().contains("urn:uuid:"));
Assert.assertTrue(client.getResponseBody().contains("Second-"));
String timeoutValue =
client.getResponseBody().substring(client.getResponseBody().indexOf("Second-"));
timeoutValue = timeoutValue.substring("Second-".length(),
timeoutValue.indexOf('<'));
@@ -929,7 +929,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.processRequest(true);
Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS,
client.getStatusCode());
// Verify all the shared locks are cleared
-
Assert.assertFalse(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertFalse(client.getResponseBody().contains("urn:uuid:"));
validateXml(client.getResponseBody());
}
@@ -969,7 +969,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.connect();
client.processRequest(true);
Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
-
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertTrue(client.getResponseBody().contains("urn:uuid:"));
String lockToken = null;
for (String header : client.getResponseHeaders()) {
if (header.startsWith("Lock-Token: ")) {
@@ -986,7 +986,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.connect();
client.processRequest(true);
Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
-
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertTrue(client.getResponseBody().contains("urn:uuid:"));
String lockToken2 = null;
for (String header : client.getResponseHeaders()) {
if (header.startsWith("Lock-Token: ")) {
@@ -1003,7 +1003,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.connect();
client.processRequest(true);
Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
-
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertTrue(client.getResponseBody().contains("urn:uuid:"));
String lockToken3 = null;
for (String header : client.getResponseHeaders()) {
if (header.startsWith("Lock-Token: ")) {
@@ -1014,9 +1014,9 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.setRequest(new String[] { "PUT
/myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" +
SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
- "If: </myfolder/myfolder3/myfolder6>
(<opaquelocktoken:5e1e2275b1cd9c17845e7e08>)" + // Obvious wrong token
+ "If: </myfolder/myfolder3/myfolder6>
(<urn:uuid:5e1e2275b1cd9c17845e7e08>)" + // Obvious wrong token
" </myfolder/myfolder7/myfolder8/myfolder9> (" + lockToken + "
" + lockToken2 + " " + lockToken3 + ")" + // lockToken is not there
- " </myfolder/myfolder2/myfolder4>
(<opaquelocktoken:7329872398754923752> [W/\"4-1729375899470\"])" + // Not locked
+ " </myfolder/myfolder2/myfolder4>
(<urn:uuid:7329872398754923752> [W/\"4-1729375899470\"])" + // Not locked
" </myfolder/myfolder7/myfolder8> (" + lockToken + ")" +
SimpleHttpClient.CRLF + // lockToken is not there
"Content-Length: 6" + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
@@ -1027,8 +1027,8 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.setRequest(new String[] { "PUT
/myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" +
SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
- "If: </myfolder/myfolder3/myfolder6>
(<opaquelocktoken:5e1e2275b1cd9c17845e7e08>)" + // Obvious wrong token
- " </myfolder/myfolder2/myfolder4>
(<opaquelocktoken:7329872398754923752> [W/\"4-1729375899470\"])" + // Not locked
+ "If: </myfolder/myfolder3/myfolder6>
(<urn:uuid:5e1e2275b1cd9c17845e7e08>)" + // Obvious wrong token
+ " </myfolder/myfolder2/myfolder4>
(<urn:uuid:7329872398754923752> [W/\"4-1729375899470\"])" + // Not locked
" </myfolder/myfolder7/myfolder8> (" + lockToken + ")" + //
lockToken is not there
" </myfolder/myfolder7/myfolder8/myfolder9> (" + lockToken2 +
" " + lockToken3 + ")" + SimpleHttpClient.CRLF + // Correct
"Content-Length: 6" + SimpleHttpClient.CRLF +
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 4c31250a78..8e726d97e1 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -211,6 +211,11 @@
Add debug logging for the web resource cache so the current size can be
tracked as resources are added and removed. (markt)
</add>
+ <update>
+ Replace legacy WebDAV <code>opaquelocktoken:</code> scheme for lock
+ tokens with <code>urn:uuid:</code> as recommended by RFC 4918, and
+ remove <code>secret</code> init parameter. (remm)
+ </update>
</changelog>
</subsection>
<subsection name="Coyote">
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]