This is an automated email from the ASF dual-hosted git repository.
hello-stephen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new c9f2dd64508 [fix](load) Avoid global BE auth precheck for stream load
(#66629)
c9f2dd64508 is described below
commit c9f2dd6450891c304fb5bede898a3a245c808d42
Author: Gavin Chou <[email protected]>
AuthorDate: Wed Aug 12 15:13:49 2026 +0800
[fix](load) Avoid global BE auth precheck for stream load (#66629)
## Summary
- Remove the generic BE HTTP auth precheck from Stream Load, Stream Load
2PC, and HTTP Stream.
- Keep the existing load-specific path: BE parses Basic auth and FE load
RPCs check LOAD against the actual db/table/txn.
- Add docker regression coverage for table-level and database-level LOAD
users with BE enable_all_http_auth both off and on.
## Testing
- sh format_code.sh on changed BE files
- git diff --check
- ninja -C be/ut_build_ASAN stream_load.cpp.o stream_load_2pc.cpp.o
http_stream.cpp.o
- run-regression-test selected both new docker suites; local regression
config skips docker execution, but framework compilation and suite
discovery passed
Co-authored-by: gavinchou <[email protected]>
---
be/src/service/http/action/http_stream.cpp | 14 +-
be/src/service/http/action/http_stream.h | 6 +-
be/src/service/http/action/stream_load.cpp | 15 +-
be/src/service/http/action/stream_load.h | 6 +-
be/src/service/http/action/stream_load_2pc.cpp | 8 +-
be/src/service/http/action/stream_load_2pc.h | 5 +-
.../test_dml_stream_load_be_auth_docker.groovy | 321 +++++++++++++++++++++
7 files changed, 345 insertions(+), 30 deletions(-)
diff --git a/be/src/service/http/action/http_stream.cpp
b/be/src/service/http/action/http_stream.cpp
index cd80517942e..ce33b3d8ef6 100644
--- a/be/src/service/http/action/http_stream.cpp
+++ b/be/src/service/http/action/http_stream.cpp
@@ -92,10 +92,10 @@
DEFINE_COUNTER_METRIC_PROTOTYPE_2ARG(http_stream_requests_total, MetricUnit::REQ
DEFINE_COUNTER_METRIC_PROTOTYPE_2ARG(http_stream_duration_ms,
MetricUnit::MILLISECONDS);
DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(http_stream_current_processing,
MetricUnit::REQUESTS);
-HttpStreamAction::HttpStreamAction(ExecEnv* exec_env)
- : HttpHandlerWithAuth(exec_env, TPrivilegeHier::GLOBAL,
TPrivilegeType::LOAD) {
- // Use LOAD privilege type: requires LOAD permission
- // Note: _exec_env is set by parent class HttpHandlerWithAuth
+HttpStreamAction::HttpStreamAction(ExecEnv* exec_env) : _exec_env(exec_env) {
+ // HTTP stream derives db/table from the SQL header and then forwards the
+ // request credentials to FE load RPCs for table-scoped LOAD checks. The
generic
+ // BE HTTP auth hook only supports caller-provided resource metadata.
_http_stream_entity =
DorisMetrics::instance()->metric_registry()->register_entity("http_stream");
INT_COUNTER_METRIC_REGISTER(_http_stream_entity,
http_stream_requests_total);
@@ -182,12 +182,6 @@ Status HttpStreamAction::_handle(HttpRequest* http_req,
std::shared_ptr<StreamLo
}
int HttpStreamAction::on_header(HttpRequest* req) {
- // Call parent's auth check first
- int ret = HttpHandlerWithAuth::on_header(req);
- if (ret != 0) {
- return ret; // Auth failed, return error
- }
-
http_stream_current_processing->increment(1);
std::shared_ptr<StreamLoadContext> ctx =
std::make_shared<StreamLoadContext>(_exec_env);
diff --git a/be/src/service/http/action/http_stream.h
b/be/src/service/http/action/http_stream.h
index 03b6a7539a2..5ecb19740fb 100644
--- a/be/src/service/http/action/http_stream.h
+++ b/be/src/service/http/action/http_stream.h
@@ -22,7 +22,7 @@
#include <functional>
#include "load/message_body_sink.h"
-#include "service/http/http_handler_with_auth.h"
+#include "service/http/http_handler.h"
#include "util/client_cache.h"
namespace doris {
@@ -31,7 +31,7 @@ class ExecEnv;
class Status;
class StreamLoadContext;
-class HttpStreamAction : public HttpHandlerWithAuth {
+class HttpStreamAction : public HttpHandler {
public:
HttpStreamAction(ExecEnv* exec_env);
~HttpStreamAction() override;
@@ -53,6 +53,8 @@ private:
Status _handle_group_commit(HttpRequest* http_req,
std::shared_ptr<StreamLoadContext> ctx);
private:
+ ExecEnv* _exec_env;
+
std::shared_ptr<MetricEntity> _http_stream_entity;
IntCounter* http_stream_requests_total;
IntCounter* http_stream_duration_ms;
diff --git a/be/src/service/http/action/stream_load.cpp
b/be/src/service/http/action/stream_load.cpp
index 1b67543f215..81d1fe9ccef 100644
--- a/be/src/service/http/action/stream_load.cpp
+++ b/be/src/service/http/action/stream_load.cpp
@@ -94,9 +94,11 @@ static const std::string ASYNC_MODE = "async_mode";
TStreamLoadPutResult k_stream_load_put_result;
#endif
-StreamLoadAction::StreamLoadAction(ExecEnv* exec_env)
- : HttpHandlerWithAuth(exec_env, TPrivilegeHier::GLOBAL,
TPrivilegeType::LOAD) {
- // Use LOAD privilege type: requires LOAD permission
+StreamLoadAction::StreamLoadAction(ExecEnv* exec_env) : _exec_env(exec_env) {
+ // Stream load forwards the parsed HTTP credentials to FE load RPCs, where
LOAD
+ // privilege is checked against the actual db/table/txn. A generic BE HTTP
+ // pre-check cannot model every stream-load variant and would duplicate
that
+ // resource-scoped authorization.
_stream_load_entity =
DorisMetrics::instance()->metric_registry()->register_entity("stream_load");
INT_COUNTER_METRIC_REGISTER(_stream_load_entity,
streaming_load_requests_total);
@@ -240,13 +242,6 @@ void
StreamLoadAction::_send_reply(std::shared_ptr<StreamLoadContext> ctx, HttpR
}
int StreamLoadAction::on_header(HttpRequest* req) {
- // Call parent's auth check first
- int ret = HttpHandlerWithAuth::on_header(req);
- if (ret != 0) {
- return ret; // Auth failed, return error
- }
-
- // Continue with stream load specific header processing
req->mark_send_reply();
streaming_load_current_processing->increment(1);
diff --git a/be/src/service/http/action/stream_load.h
b/be/src/service/http/action/stream_load.h
index 89b18f045cb..29fc92065ed 100644
--- a/be/src/service/http/action/stream_load.h
+++ b/be/src/service/http/action/stream_load.h
@@ -22,7 +22,7 @@
#include <string>
#include "common/metrics/metrics.h"
-#include "service/http/http_handler_with_auth.h"
+#include "service/http/http_handler.h"
namespace doris {
@@ -31,7 +31,7 @@ class Status;
class StreamLoadContext;
class HttpRequest;
-class StreamLoadAction : public HttpHandlerWithAuth {
+class StreamLoadAction : public HttpHandler {
public:
StreamLoadAction(ExecEnv* exec_env);
~StreamLoadAction() override;
@@ -59,6 +59,8 @@ private:
void _send_reply(std::shared_ptr<StreamLoadContext> ctx, HttpRequest* req);
private:
+ ExecEnv* _exec_env;
+
std::shared_ptr<MetricEntity> _stream_load_entity;
IntCounter* streaming_load_requests_total;
IntCounter* streaming_load_duration_ms;
diff --git a/be/src/service/http/action/stream_load_2pc.cpp
b/be/src/service/http/action/stream_load_2pc.cpp
index 0239ed9fee6..15ca8066cf9 100644
--- a/be/src/service/http/action/stream_load_2pc.cpp
+++ b/be/src/service/http/action/stream_load_2pc.cpp
@@ -39,10 +39,10 @@
namespace doris {
-StreamLoad2PCAction::StreamLoad2PCAction(ExecEnv* exec_env)
- : HttpHandlerWithAuth(exec_env, TPrivilegeHier::GLOBAL,
TPrivilegeType::LOAD) {
- // Use LOAD privilege type: requires LOAD permission
- // Note: _exec_env is set by parent class HttpHandlerWithAuth
+StreamLoad2PCAction::StreamLoad2PCAction(ExecEnv* exec_env) :
_exec_env(exec_env) {
+ // 2PC commit/abort resolves the transaction's table list in FE and checks
LOAD
+ // privilege for each table there. A BE HTTP pre-check may only have
db/label or
+ // txn_id, so it would be less accurate and can reject valid table-scoped
users.
}
void StreamLoad2PCAction::handle(HttpRequest* req) {
diff --git a/be/src/service/http/action/stream_load_2pc.h
b/be/src/service/http/action/stream_load_2pc.h
index 939e9f8dbfa..3e1b3076eee 100644
--- a/be/src/service/http/action/stream_load_2pc.h
+++ b/be/src/service/http/action/stream_load_2pc.h
@@ -19,14 +19,14 @@
#include <string>
-#include "service/http/http_handler_with_auth.h"
+#include "service/http/http_handler.h"
namespace doris {
class ExecEnv;
class HttpRequest;
-class StreamLoad2PCAction : public HttpHandlerWithAuth {
+class StreamLoad2PCAction : public HttpHandler {
public:
StreamLoad2PCAction(ExecEnv* exec_env);
@@ -36,6 +36,7 @@ public:
std::string get_success_info(const std::string msg, const std::string
txn_operation);
private:
+ ExecEnv* _exec_env;
};
} // namespace doris
diff --git
a/regression-test/suites/auth_call/test_dml_stream_load_be_auth_docker.groovy
b/regression-test/suites/auth_call/test_dml_stream_load_be_auth_docker.groovy
new file mode 100644
index 00000000000..46d06e5df81
--- /dev/null
+++
b/regression-test/suites/auth_call/test_dml_stream_load_be_auth_docker.groovy
@@ -0,0 +1,321 @@
+// 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.
+
+import org.apache.doris.regression.suite.ClusterOptions
+
+suite("test_dml_stream_load_be_auth_off_docker", "docker,auth_call") {
+ def options = new ClusterOptions()
+ options.cloudMode = true
+ options.feNum = 1
+ options.beNum = 1
+
+ docker(options) {
+ String suiteName = "test_dml_stream_load_be_auth_off_docker"
+ String tableUser = "${suiteName}_table_user"
+ String dbUser = "${suiteName}_db_user"
+ String pwd = "C123_567p"
+ String dbName = "${suiteName}_db"
+ String tableName = "${suiteName}_tb"
+ String otherTableName = "${suiteName}_other_tb"
+ String dataFile =
"${context.file.parent}/../../data/auth_call/stream_load_data.csv"
+
+ def fe = cluster.getFeByIndex(1)
+ def be = cluster.getAllBackends(true).get(0)
+ String feHttpAddress = "${fe.host}:${fe.httpPort}"
+ String beHttpAddress = "${be.host}:${be.httpPort}"
+
+ def parseCurlResult = { String out, String err ->
+ def lines = out.readLines()
+ assertTrue("curl output should include http code, output=${out},
err=${err}", !lines.isEmpty())
+ def httpCode = lines.last() as int
+ def body = lines.size() == 1 ? "" : lines[0..-2].join("\n")
+ return [httpCode, body, err]
+ }
+
+ def streamLoad = { String authUser, String endpoint, String table,
String label,
+ boolean followRedirect, List extraHeaders ->
+ def command = [
+ "curl", "--noproxy", "*", "-sS", "-w", "\n%{http_code}",
+ "-u", "${authUser}:${pwd}",
+ "-H", "label:${label}",
+ "-H", "column_separator:,",
+ ]
+ if (followRedirect) {
+ command.add("--location-trusted")
+ }
+ extraHeaders.each { command.addAll(["-H", it]) }
+ command.addAll([
+ "-T", dataFile,
+ "http://${endpoint}/api/${dbName}/${table}/_stream_load"
+ ])
+ logger.info("stream load target: ${endpoint}, table: ${table},
label: ${label}, user: ${authUser}")
+ def process = command.execute()
+ process.waitForOrKill(7200000)
+ def out = process.text.trim()
+ def err = process.errorStream.text.trim()
+ logger.info("stream load out: ${out}, err: ${err}")
+ return parseCurlResult(out, err)
+ }
+
+ def streamLoad2pc = { String authUser, String table, def txnId, String
txnOperation ->
+ def command = [
+ "curl", "--noproxy", "*", "-sS", "-w", "\n%{http_code}",
+ "-X", "PUT",
+ "-u", "${authUser}:${pwd}",
+ "-H", "txn_id:${txnId}",
+ "-H", "txn_operation:${txnOperation}",
+
"http://${beHttpAddress}/api/${dbName}/${table}/_stream_load_2pc"
+ ]
+ logger.info("stream load 2pc target: ${beHttpAddress}, table:
${table}, "
+ + "txn operation: ${txnOperation}, user: ${authUser}")
+ def process = command.execute()
+ process.waitForOrKill(7200000)
+ def out = process.text.trim()
+ def err = process.errorStream.text.trim()
+ logger.info("stream load 2pc out: ${out}, err: ${err}")
+ return parseCurlResult(out, err)
+ }
+
+ def assertLoadFailed = { def result ->
+ assertEquals(200, result[0])
+ def json = parseJson(result[1])
+ assertTrue("stream load should fail, body=${result[1]}",
json.Status != "Success")
+ }
+
+ def assertLoadSuccess = { def result ->
+ assertEquals(200, result[0])
+ def json = parseJson(result[1])
+ assertEquals("Success", json.Status)
+ assertEquals(3, json.NumberTotalRows)
+ assertEquals(3, json.NumberLoadedRows)
+ return json
+ }
+
+ try {
+ try_sql("DROP USER ${tableUser}")
+ try_sql("DROP USER ${dbUser}")
+ sql """DROP DATABASE IF EXISTS ${dbName}"""
+ sql """CREATE USER '${tableUser}' IDENTIFIED BY '${pwd}'"""
+ sql """CREATE USER '${dbUser}' IDENTIFIED BY '${pwd}'"""
+ def clusters = sql """SHOW CLUSTERS"""
+ assertTrue(!clusters.isEmpty())
+ sql """GRANT USAGE_PRIV ON CLUSTER `${clusters[0][0]}` TO
${tableUser}"""
+ sql """GRANT USAGE_PRIV ON CLUSTER `${clusters[0][0]}` TO
${dbUser}"""
+
+ sql """CREATE DATABASE ${dbName}"""
+ sql """
+ CREATE TABLE ${dbName}.${tableName} (
+ id BIGINT,
+ username VARCHAR(20)
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+ sql """
+ CREATE TABLE ${dbName}.${otherTableName} (
+ id BIGINT,
+ username VARCHAR(20)
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+
+ assertLoadFailed(streamLoad(tableUser, beHttpAddress, tableName,
"off_denied_before_grant",
+ false, []))
+
+ sql """GRANT LOAD_PRIV ON ${dbName}.${tableName} TO ${tableUser}"""
+
+ assertLoadSuccess(streamLoad(tableUser, beHttpAddress, tableName,
"off_table_grant_be",
+ false, []))
+ assertLoadSuccess(streamLoad(tableUser, feHttpAddress, tableName,
"off_table_grant_fe",
+ true, []))
+ def prepared = assertLoadSuccess(streamLoad(tableUser,
beHttpAddress, tableName,
+ "off_table_grant_2pc", false, ["two_phase_commit:true"]))
+ def committed = streamLoad2pc(tableUser, tableName,
prepared.TxnId, "commit")
+ assertEquals(200, committed[0])
+ assertEquals("Success", parseJson(committed[1]).status)
+
+ assertLoadFailed(streamLoad(tableUser, beHttpAddress,
otherTableName,
+ "off_denied_other_table", false, []))
+
+ sql """GRANT LOAD_PRIV ON ${dbName}.* TO ${dbUser}"""
+ assertLoadSuccess(streamLoad(dbUser, beHttpAddress,
otherTableName, "off_db_grant_be",
+ false, []))
+
+ def tableCount = sql """SELECT COUNT(*) FROM
${dbName}.${tableName}"""
+ assertEquals(9, tableCount[0][0] as int)
+ def otherTableCount = sql """SELECT COUNT(*) FROM
${dbName}.${otherTableName}"""
+ assertEquals(3, otherTableCount[0][0] as int)
+ } finally {
+ try_sql """DROP DATABASE IF EXISTS ${dbName}"""
+ try_sql("DROP USER ${tableUser}")
+ try_sql("DROP USER ${dbUser}")
+ }
+ }
+}
+
+suite("test_dml_stream_load_be_auth_on_docker", "docker,auth_call") {
+ def options = new ClusterOptions()
+ options.cloudMode = true
+ options.feNum = 1
+ options.beNum = 1
+ options.beConfigs.add("enable_all_http_auth=true")
+
+ docker(options) {
+ String suiteName = "test_dml_stream_load_be_auth_on_docker"
+ String tableUser = "${suiteName}_table_user"
+ String dbUser = "${suiteName}_db_user"
+ String pwd = "C123_567p"
+ String dbName = "${suiteName}_db"
+ String tableName = "${suiteName}_tb"
+ String otherTableName = "${suiteName}_other_tb"
+ String dataFile =
"${context.file.parent}/../../data/auth_call/stream_load_data.csv"
+
+ def fe = cluster.getFeByIndex(1)
+ def be = cluster.getAllBackends(true).get(0)
+ String feHttpAddress = "${fe.host}:${fe.httpPort}"
+ String beHttpAddress = "${be.host}:${be.httpPort}"
+
+ def parseCurlResult = { String out, String err ->
+ def lines = out.readLines()
+ assertTrue("curl output should include http code, output=${out},
err=${err}", !lines.isEmpty())
+ def httpCode = lines.last() as int
+ def body = lines.size() == 1 ? "" : lines[0..-2].join("\n")
+ return [httpCode, body, err]
+ }
+
+ def streamLoad = { String authUser, String endpoint, String table,
String label,
+ boolean followRedirect, List extraHeaders ->
+ def command = [
+ "curl", "--noproxy", "*", "-sS", "-w", "\n%{http_code}",
+ "-u", "${authUser}:${pwd}",
+ "-H", "label:${label}",
+ "-H", "column_separator:,",
+ ]
+ if (followRedirect) {
+ command.add("--location-trusted")
+ }
+ extraHeaders.each { command.addAll(["-H", it]) }
+ command.addAll([
+ "-T", dataFile,
+ "http://${endpoint}/api/${dbName}/${table}/_stream_load"
+ ])
+ logger.info("stream load target: ${endpoint}, table: ${table},
label: ${label}, user: ${authUser}")
+ def process = command.execute()
+ process.waitForOrKill(7200000)
+ def out = process.text.trim()
+ def err = process.errorStream.text.trim()
+ logger.info("stream load out: ${out}, err: ${err}")
+ return parseCurlResult(out, err)
+ }
+
+ def streamLoad2pc = { String authUser, String table, def txnId, String
txnOperation ->
+ def command = [
+ "curl", "--noproxy", "*", "-sS", "-w", "\n%{http_code}",
+ "-X", "PUT",
+ "-u", "${authUser}:${pwd}",
+ "-H", "txn_id:${txnId}",
+ "-H", "txn_operation:${txnOperation}",
+
"http://${beHttpAddress}/api/${dbName}/${table}/_stream_load_2pc"
+ ]
+ logger.info("stream load 2pc target: ${beHttpAddress}, table:
${table}, "
+ + "txn operation: ${txnOperation}, user: ${authUser}")
+ def process = command.execute()
+ process.waitForOrKill(7200000)
+ def out = process.text.trim()
+ def err = process.errorStream.text.trim()
+ logger.info("stream load 2pc out: ${out}, err: ${err}")
+ return parseCurlResult(out, err)
+ }
+
+ def assertLoadFailed = { def result ->
+ assertEquals(200, result[0])
+ def json = parseJson(result[1])
+ assertTrue("stream load should fail, body=${result[1]}",
json.Status != "Success")
+ }
+
+ def assertLoadSuccess = { def result ->
+ assertEquals(200, result[0])
+ def json = parseJson(result[1])
+ assertEquals("Success", json.Status)
+ assertEquals(3, json.NumberTotalRows)
+ assertEquals(3, json.NumberLoadedRows)
+ return json
+ }
+
+ try {
+ try_sql("DROP USER ${tableUser}")
+ try_sql("DROP USER ${dbUser}")
+ sql """DROP DATABASE IF EXISTS ${dbName}"""
+ sql """CREATE USER '${tableUser}' IDENTIFIED BY '${pwd}'"""
+ sql """CREATE USER '${dbUser}' IDENTIFIED BY '${pwd}'"""
+ def clusters = sql """SHOW CLUSTERS"""
+ assertTrue(!clusters.isEmpty())
+ sql """GRANT USAGE_PRIV ON CLUSTER `${clusters[0][0]}` TO
${tableUser}"""
+ sql """GRANT USAGE_PRIV ON CLUSTER `${clusters[0][0]}` TO
${dbUser}"""
+
+ sql """CREATE DATABASE ${dbName}"""
+ sql """
+ CREATE TABLE ${dbName}.${tableName} (
+ id BIGINT,
+ username VARCHAR(20)
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+ sql """
+ CREATE TABLE ${dbName}.${otherTableName} (
+ id BIGINT,
+ username VARCHAR(20)
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+
+ assertLoadFailed(streamLoad(tableUser, beHttpAddress, tableName,
"on_denied_before_grant",
+ false, []))
+
+ sql """GRANT LOAD_PRIV ON ${dbName}.${tableName} TO ${tableUser}"""
+
+ assertLoadSuccess(streamLoad(tableUser, beHttpAddress, tableName,
"on_table_grant_be",
+ false, []))
+ assertLoadSuccess(streamLoad(tableUser, feHttpAddress, tableName,
"on_table_grant_fe",
+ true, []))
+ def prepared = assertLoadSuccess(streamLoad(tableUser,
beHttpAddress, tableName,
+ "on_table_grant_2pc", false, ["two_phase_commit:true"]))
+ def committed = streamLoad2pc(tableUser, tableName,
prepared.TxnId, "commit")
+ assertEquals(200, committed[0])
+ assertEquals("Success", parseJson(committed[1]).status)
+
+ assertLoadFailed(streamLoad(tableUser, beHttpAddress,
otherTableName,
+ "on_denied_other_table", false, []))
+
+ sql """GRANT LOAD_PRIV ON ${dbName}.* TO ${dbUser}"""
+ assertLoadSuccess(streamLoad(dbUser, beHttpAddress,
otherTableName, "on_db_grant_be",
+ false, []))
+
+ def tableCount = sql """SELECT COUNT(*) FROM
${dbName}.${tableName}"""
+ assertEquals(9, tableCount[0][0] as int)
+ def otherTableCount = sql """SELECT COUNT(*) FROM
${dbName}.${otherTableName}"""
+ assertEquals(3, otherTableCount[0][0] as int)
+ } finally {
+ try_sql """DROP DATABASE IF EXISTS ${dbName}"""
+ try_sql("DROP USER ${tableUser}")
+ try_sql("DROP USER ${dbUser}")
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]