diff --git a/.github/workflows/build-docker-images.yml b/.github/workflows/build-docker-images.yml index c0e48b7..55e1186 100644 --- a/.github/workflows/build-docker-images.yml +++ b/.github/workflows/build-docker-images.yml @@ -28,7 +28,7 @@ jobs: - name: Install build requirements run: | - brew install boost + brew install boost gflags - name: Configure Project uses: threeal/cmake-action@v1.3.0 @@ -36,21 +36,31 @@ jobs: generator: Ninja run-build: true - - name: Sign and notarize the release build - uses: toitlang/action-macos-sign-notarize@v1.1.1 + - name: Sign and notarize the server release build + uses: prmoore77/action-macos-sign-notarize@b0f525e0d98a47b0884558b786f21453889a04d7 with: certificate: ${{ secrets.APPLE_CERTIFICATE }} certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} username: ${{ secrets.APPLE_ID_USERNAME }} password: ${{ secrets.APPLE_ID_PASSWORD }} apple-team-id: ${{ secrets.APPLE_TEAM_ID }} - app-path: build/flight_sql + app-path: build/flight_sql_server entitlements-path: macos/entitlements.plist + - name: Sign and notarize the server release build + uses: prmoore77/action-macos-sign-notarize@b0f525e0d98a47b0884558b786f21453889a04d7 + with: + certificate: ${{ secrets.APPLE_CERTIFICATE }} + certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + username: ${{ secrets.APPLE_ID_USERNAME }} + password: ${{ secrets.APPLE_ID_PASSWORD }} + apple-team-id: ${{ secrets.APPLE_TEAM_ID }} + app-path: build/flight_sql_client + - name: Zip artifacts run: | - mv build/flight_sql . - zip -j ${{ env.zip_file_name }} flight_sql + mv build/flight_sql_server build/flight_sql_client . + zip -j ${{ env.zip_file_name }} flight_sql_server flight_sql_client - name: Upload artifacts uses: actions/upload-artifact@v4 @@ -86,7 +96,8 @@ jobs: cmake \ gcc \ git \ - libboost-all-dev + libboost-all-dev \ + libgflags-dev sudo apt-get clean sudo rm -rf /var/lib/apt/lists/* @@ -98,8 +109,8 @@ jobs: - name: Zip artifacts run: | - mv build/flight_sql . - zip -j ${{ env.zip_file_name }} flight_sql + mv build/flight_sql_server build/flight_sql_client . + zip -j ${{ env.zip_file_name }} flight_sql_server flight_sql_client - name: Upload artifacts uses: actions/upload-artifact@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 0df3745..800d407 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,8 +134,6 @@ target_include_directories(flightsqlserver PRIVATE target_link_libraries(flightsqlserver PRIVATE Threads::Threads - Arrow::arrow_static - ArrowFlight::arrow_flight_static ArrowFlightSql::arrow_flight_sql_static sqlite duckdb @@ -156,18 +154,43 @@ install(TARGETS flightsqlserver PUBLIC_HEADER DESTINATION include ) -# ------------ Executable section ------------ -add_executable(flight_sql - src/flight_sql.cpp +# ------------ Server Executable section ------------ +add_executable(flight_sql_server + src/flight_sql_server.cpp ) -target_link_libraries(flight_sql PRIVATE +target_link_libraries(flight_sql_server PRIVATE flightsqlserver ${Boost_LIBRARIES} ) -target_compile_options(flight_sql PRIVATE "-static") +target_compile_options(flight_sql_server PRIVATE "-static") -install(TARGETS flight_sql +install(TARGETS flight_sql_server + DESTINATION bin +) + +# ------------ Client Executable section ------------ +add_executable(flight_sql_client + src/flight_sql_client.cpp +) + +target_link_libraries(flight_sql_client PRIVATE + Threads::Threads + ArrowFlightSql::arrow_flight_sql_static + ${Boost_LIBRARIES} + "-lresolv" +) + +if (APPLE) + # macOS-specific libraries and options + target_link_libraries(flight_sql_client PRIVATE "-framework CoreFoundation") +elseif (UNIX AND NOT APPLE) + target_link_libraries(flight_sql_client PRIVATE "-lssl -lcrypto") +endif () + +target_compile_options(flight_sql_client PRIVATE "-static") + +install(TARGETS flight_sql_client DESTINATION bin ) diff --git a/Dockerfile b/Dockerfile index 684001d..b5ec6a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11.7 +FROM python:3.11.8 ARG TARGETPLATFORM ARG TARGETARCH @@ -19,6 +19,7 @@ RUN apt-get update && \ git \ ninja-build \ libboost-all-dev \ + libgflags-dev \ sqlite3 \ vim && \ apt-get clean && \ diff --git a/Dockerfile.ci b/Dockerfile.ci index 131f965..d5e39b4 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1,4 +1,4 @@ -FROM python:3.11.7 +FROM python:3.11.8 ARG TARGETPLATFORM ARG TARGETARCH @@ -72,9 +72,11 @@ RUN python "scripts/create_duckdb_database_file.py" \ --overwrite-file=true \ --scale-factor=0.01 -COPY --chown=app_user:app_user flight_sql /usr/local/bin/flight_sql +COPY --chown=app_user:app_user flight_sql_server /usr/local/bin/flight_sql_server +RUN chmod +x /usr/local/bin/flight_sql_server -RUN chmod +x /usr/local/bin/flight_sql +COPY --chown=app_user:app_user flight_sql_client /usr/local/bin/flight_sql_client +RUN chmod +x /usr/local/bin/flight_sql_client COPY --chown=app_user:app_user tls tls diff --git a/README.md b/README.md index 2e1da44..68a0640 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,36 @@ n_nationkey: [[24]] n_name: [["UNITED STATES"]] ``` +### Connecting via the new `flight_sql_client` CLI tool +You can also use the new `flight_sql_client` CLI tool to connect to the Flight SQL server, and then run a single command. This tool is built into the Docker image, and is also available as a standalone executable for Linux and MacOS. + +Example (run from the host computer's terminal): +```bash +flight_sql_client \ + --command Execute \ + --host "localhost" \ + --port 31337 \ + --username "flight_username" \ + --password "flight_password" \ + --query "SELECT version()" \ + --use-tls \ + --tls-skip-verify +``` + +That should return: +```text +Results from endpoint 1 of 1 +Schema: +version(): string + +Results: +version(): [ + "v0.10.0" + ] + +Total: 1 +``` + ### Tear-down Stop the docker image with: ```bash @@ -179,7 +209,7 @@ docker stop flight-sql ## Option 2 - Download and run the flight_sql CLI executable -Download (and unzip) the latest release of the **flight_sql** CLI executable from these currently supported platforms: +Download (and unzip) the latest release of the **flight_sql_server** CLI executable from these currently supported platforms: [Linux x86-64](https://github.com/voltrondata/flight-sql-server-example/releases/latest/download/flight_sql_cli_linux_amd64.zip) [Linux arm64](https://github.com/voltrondata/flight-sql-server-example/releases/latest/download/flight_sql_cli_linux_arm64.zip) [MacOS x86-64](https://github.com/voltrondata/flight-sql-server-example/releases/latest/download/flight_sql_cli_macos_amd64.zip) @@ -187,12 +217,12 @@ Download (and unzip) the latest release of the **flight_sql** CLI executable fro Then from a terminal - you can run: ```bash -FLIGHT_PASSWORD="flight_password" flight_sql --database-filename data/some_db.duckdb --print-queries +FLIGHT_PASSWORD="flight_password" flight_sql_server --database-filename data/some_db.duckdb --print-queries ``` To see all program options - run: ```bash -flight_sql --help +flight_sql_server --help ``` ## Option 3 - Steps to build the solution manually @@ -243,14 +273,14 @@ popd 6. Start the Flight SQL server (and print client SQL commands as they run using the --print-queries option) ```bash -FLIGHT_PASSWORD="flight_password" flight_sql --database-filename data/TPC-H-small.duckdb --print-queries +FLIGHT_PASSWORD="flight_password" flight_sql_server --database-filename data/TPC-H-small.duckdb --print-queries ``` ## Selecting different backends This option allows choosing from two backends: SQLite and DuckDB. It defaults to DuckDB. ```bash -$ FLIGHT_PASSWORD="flight_password" flight_sql --database-filename data/TPC-H-small.duckdb +$ FLIGHT_PASSWORD="flight_password" flight_sql_server --database-filename data/TPC-H-small.duckdb Apache Arrow version: 15.0.0 WARNING - TLS is disabled for the Flight SQL server - this is insecure. DuckDB version: v0.10.0 @@ -264,14 +294,14 @@ Apache Arrow Flight SQL server - with engine: DuckDB - will listen on grpc+tcp:/ Flight SQL server - started ``` -The above call is equivalent to running `flight_sql -B duckdb` or `flight_sql --backend duckdb`. To select SQLite run +The above call is equivalent to running `flight_sql_server -B duckdb` or `flight_sql --backend duckdb`. To select SQLite run ```bash -FLIGHT_PASSWORD="flight_password" flight_sql -B sqlite -D data/TPC-H-small.sqlite +FLIGHT_PASSWORD="flight_password" flight_sql_server -B sqlite -D data/TPC-H-small.sqlite ``` or ```bash -FLIGHT_PASSWORD="flight_password" flight_sql --backend sqlite --database-filename data/TPC-H-small.sqlite +FLIGHT_PASSWORD="flight_password" flight_sql_server --backend sqlite --database-filename data/TPC-H-small.sqlite ``` The above will produce the following: @@ -286,10 +316,10 @@ Flight SQL server - started ``` ## Print help -To see all the available options run `flight_sql --help`. +To see all the available options run `flight_sql_server --help`. ```bash -flight_sql --help +flight_sql_server --help Allowed options: --help produce this help message --version Print the version and exit diff --git a/requirements.txt b/requirements.txt index 17768ad..0472f34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ pandas==2.1.* duckdb==0.10.0 click==8.1.* pyarrow==15.0.0 -adbc-driver-flightsql==0.9.* -adbc-driver-manager==0.9.* +adbc-driver-flightsql==0.10.* +adbc-driver-manager==0.10.* diff --git a/scripts/start_flight_sql.sh b/scripts/start_flight_sql.sh index b075dfc..89dc36b 100755 --- a/scripts/start_flight_sql.sh +++ b/scripts/start_flight_sql.sh @@ -30,4 +30,4 @@ then PRINT_QUERIES_FLAG="--print-queries" fi -flight_sql --backend="${L_DATABASE_BACKEND}" --database-filename="${L_DATABASE_FILENAME}" ${TLS_ARG} ${PRINT_QUERIES_FLAG} +flight_sql_server --backend="${L_DATABASE_BACKEND}" --database-filename="${L_DATABASE_FILENAME}" ${TLS_ARG} ${PRINT_QUERIES_FLAG} diff --git a/scripts/test_flight_sql.py b/scripts/test_flight_sql.py index df3996c..1c15811 100644 --- a/scripts/test_flight_sql.py +++ b/scripts/test_flight_sql.py @@ -1,17 +1,39 @@ import os +from time import sleep +import pyarrow from adbc_driver_flightsql import dbapi as flight_sql, DatabaseOptions -flight_password = os.getenv("FLIGHT_PASSWORD") -with flight_sql.connect(uri="grpc+tls://localhost:31337", - db_kwargs={"username": "flight_username", - "password": flight_password, - DatabaseOptions.TLS_SKIP_VERIFY.value: "true" # Not needed if you use a trusted CA-signed TLS cert - } - ) as conn: - with conn.cursor() as cur: - cur.execute("SELECT n_nationkey, n_name FROM nation WHERE n_nationkey = ?", - parameters=[24] - ) - x = cur.fetch_arrow_table() - print(x) +# Setup variables +max_attempts: int = 10 +sleep_interval: int = 10 +flight_password = os.environ["FLIGHT_PASSWORD"] + +def main(): + for attempt in range(max_attempts): + try: + with flight_sql.connect(uri="grpc+tls://localhost:31337", + db_kwargs={"username": "flight_username", + "password": flight_password, + DatabaseOptions.TLS_SKIP_VERIFY.value: "true" # Not needed if you use a trusted CA-signed TLS cert + } + ) as conn: + with conn.cursor() as cur: + cur.execute("SELECT n_nationkey, n_name FROM nation WHERE n_nationkey = ?", + parameters=[24] + ) + x = cur.fetch_arrow_table() + print(x) + except Exception as e: + if attempt == max_attempts - 1: + raise e + else: + print(f"Attempt {attempt + 1} failed: {e}, sleeping for {sleep_interval} seconds") + sleep(sleep_interval) + else: + print("Success!") + break + + +if __name__ == "__main__": + main() diff --git a/scripts/test_flight_sql.sh b/scripts/test_flight_sql.sh index acdd215..b5cf05b 100755 --- a/scripts/test_flight_sql.sh +++ b/scripts/test_flight_sql.sh @@ -17,8 +17,8 @@ started="0" # Check if the process is running while [ $elapsed_time -lt $timeout_limit ]; do - # Check if the process is running using --exact to match the whole process name - if pgrep --exact "flight_sql" > /dev/null; then + # Check if the process is running + if pgrep "flight_sql" > /dev/null; then echo "Flight SQL Server process started successfully!" started="1" # Sleep for a few more seconds... diff --git a/src/flight_sql_client.cpp b/src/flight_sql_client.cpp new file mode 100644 index 0000000..b5d44cf --- /dev/null +++ b/src/flight_sql_client.cpp @@ -0,0 +1,240 @@ +// 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. + +#include +#define BOOST_NO_CXX98_FUNCTION_BASE // ARROW-17805 +#include +#include +#include +#include +#include +#include + +#include "arrow/array/builder_binary.h" +#include "arrow/array/builder_primitive.h" +#include "arrow/flight/api.h" +#include "arrow/flight/sql/api.h" +#include "arrow/io/memory.h" +#include "arrow/pretty_print.h" +#include "arrow/status.h" +#include "arrow/table.h" + +using arrow::Result; +using arrow::Schema; +using arrow::Status; +using arrow::flight::ClientAuthHandler; +using arrow::flight::FlightCallOptions; +using arrow::flight::FlightClient; +using arrow::flight::FlightDescriptor; +using arrow::flight::FlightEndpoint; +using arrow::flight::FlightInfo; +using arrow::flight::FlightStreamChunk; +using arrow::flight::FlightStreamReader; +using arrow::flight::Location; +using arrow::flight::Ticket; +using arrow::flight::sql::FlightSqlClient; +using arrow::flight::sql::TableRef; + +DEFINE_string(host, "localhost", "Host to connect to"); +DEFINE_int32(port, 31337, "Port to connect to"); +DEFINE_string(username, "", "Username"); +DEFINE_string(password, "", "Password"); +DEFINE_bool(use_tls, false, "Use TLS for connection"); +DEFINE_string(tls_roots, "", "Path to Root certificates for TLS (in PEM format)"); +DEFINE_bool(tls_skip_verify, false, "Skip TLS server certificate verification"); +DEFINE_string(mtls_cert_chain, "", "Path to Certificate chain (in PEM format) used for mTLS authentication - if server requires it, must be accompanied by mtls_private_key"); +DEFINE_string(mtls_private_key, "", "Path to Private key (in PEM format) used for mTLS authentication - if server requires it"); + +DEFINE_string(command, "", "Method to run"); +DEFINE_string(query, "", "Query"); +DEFINE_string(catalog, "", "Catalog"); +DEFINE_string(schema, "", "Schema"); +DEFINE_string(table, "", "Table"); + +Status PrintResultsForEndpoint(FlightSqlClient& client, + const FlightCallOptions& call_options, + const FlightEndpoint& endpoint) { + ARROW_ASSIGN_OR_RAISE(auto stream, client.DoGet(call_options, endpoint.ticket)); + + const arrow::Result>& schema = stream->GetSchema(); + ARROW_RETURN_NOT_OK(schema); + + std::cout << "Schema:" << std::endl; + std::cout << schema->get()->ToString() << std::endl << std::endl; + + std::cout << "Results:" << std::endl; + + int64_t num_rows = 0; + + while (true) { + ARROW_ASSIGN_OR_RAISE(FlightStreamChunk chunk, stream->Next()); + if (chunk.data == nullptr) { + break; + } + std::cout << chunk.data->ToString() << std::endl; + num_rows += chunk.data->num_rows(); + } + + std::cout << "Total: " << num_rows << std::endl; + + return Status::OK(); +} + +Status PrintResults(FlightSqlClient& client, const FlightCallOptions& call_options, + const std::unique_ptr& info) { + const std::vector& endpoints = info->endpoints(); + + for (size_t i = 0; i < endpoints.size(); i++) { + std::cout << "Results from endpoint " << i + 1 << " of " << endpoints.size() + << std::endl; + ARROW_RETURN_NOT_OK(PrintResultsForEndpoint(client, call_options, endpoints[i])); + } + + return Status::OK(); +} + +Status getPEMCertFileContents(const std::string& cert_file_path, std::string& cert_contents) { + std::ifstream cert_file(cert_file_path); + if (!cert_file.is_open()) { + return Status::IOError("Could not open file: " + cert_file_path); + } + + std::stringstream cert_stream; + cert_stream << cert_file.rdbuf(); + cert_contents = cert_stream.str(); + + return Status::OK(); +} + +Status RunMain() { + ARROW_ASSIGN_OR_RAISE(auto location, + (FLAGS_use_tls) + ? Location::ForGrpcTls( + FLAGS_host, FLAGS_port) + : Location::ForGrpcTcp( + FLAGS_host, FLAGS_port)); + + // Setup our options + arrow::flight::FlightClientOptions options; + + if (!FLAGS_tls_roots.empty()) { + ARROW_RETURN_NOT_OK(getPEMCertFileContents(FLAGS_tls_roots, options.tls_root_certs)); + } + + options.disable_server_verification = FLAGS_tls_skip_verify; + + if (!FLAGS_mtls_cert_chain.empty()) { + ARROW_RETURN_NOT_OK(getPEMCertFileContents(FLAGS_mtls_cert_chain, options.cert_chain)); + + if (!FLAGS_mtls_private_key.empty()) { + ARROW_RETURN_NOT_OK(getPEMCertFileContents(FLAGS_mtls_private_key, options.private_key)); + } + else { + return Status::Invalid("mTLS private key file must be provided if mTLS certificate chain is provided"); + } + } + + ARROW_ASSIGN_OR_RAISE(auto client, FlightClient::Connect(location, options)); + + FlightCallOptions call_options; + + if (!FLAGS_username.empty() || !FLAGS_password.empty()) { + Result> bearer_result = + client->AuthenticateBasicToken({}, FLAGS_username, FLAGS_password); + ARROW_RETURN_NOT_OK(bearer_result); + + call_options.headers.push_back(bearer_result.ValueOrDie()); + } + + FlightSqlClient sql_client(std::move(client)); + + if (FLAGS_command == "ExecuteUpdate") { + ARROW_ASSIGN_OR_RAISE(auto rows, sql_client.ExecuteUpdate(call_options, FLAGS_query)); + + std::cout << "Result: " << rows << std::endl; + + return Status::OK(); + } + + std::unique_ptr info; + + if (FLAGS_command == "Execute") { + ARROW_ASSIGN_OR_RAISE(info, sql_client.Execute(call_options, FLAGS_query)); + } else if (FLAGS_command == "GetCatalogs") { + ARROW_ASSIGN_OR_RAISE(info, sql_client.GetCatalogs(call_options)); + } else if (FLAGS_command == "PreparedStatementExecute") { + ARROW_ASSIGN_OR_RAISE(auto prepared_statement, + sql_client.Prepare(call_options, FLAGS_query)); + ARROW_ASSIGN_OR_RAISE(info, prepared_statement->Execute()); + } else if (FLAGS_command == "PreparedStatementExecuteParameterBinding") { + ARROW_ASSIGN_OR_RAISE(auto prepared_statement, sql_client.Prepare({}, FLAGS_query)); + auto parameter_schema = prepared_statement->parameter_schema(); + auto result_set_schema = prepared_statement->dataset_schema(); + + std::cout << result_set_schema->ToString(false) << std::endl; + arrow::Int64Builder int_builder; + ARROW_RETURN_NOT_OK(int_builder.Append(1)); + std::shared_ptr int_array; + ARROW_RETURN_NOT_OK(int_builder.Finish(&int_array)); + std::shared_ptr result; + result = arrow::RecordBatch::Make(parameter_schema, 1, {int_array}); + + ARROW_RETURN_NOT_OK(prepared_statement->SetParameters(result)); + ARROW_ASSIGN_OR_RAISE(info, prepared_statement->Execute()); + } else if (FLAGS_command == "GetDbSchemas") { + ARROW_ASSIGN_OR_RAISE( + info, sql_client.GetDbSchemas(call_options, &FLAGS_catalog, &FLAGS_schema)); + } else if (FLAGS_command == "GetTableTypes") { + ARROW_ASSIGN_OR_RAISE(info, sql_client.GetTableTypes(call_options)); + } else if (FLAGS_command == "GetTables") { + ARROW_ASSIGN_OR_RAISE( + info, sql_client.GetTables(call_options, &FLAGS_catalog, &FLAGS_schema, + &FLAGS_table, false, nullptr)); + } else if (FLAGS_command == "GetExportedKeys") { + TableRef table_ref = {std::make_optional(FLAGS_catalog), + std::make_optional(FLAGS_schema), FLAGS_table}; + ARROW_ASSIGN_OR_RAISE(info, sql_client.GetExportedKeys(call_options, table_ref)); + } else if (FLAGS_command == "GetImportedKeys") { + TableRef table_ref = {std::make_optional(FLAGS_catalog), + std::make_optional(FLAGS_schema), FLAGS_table}; + ARROW_ASSIGN_OR_RAISE(info, sql_client.GetImportedKeys(call_options, table_ref)); + } else if (FLAGS_command == "GetPrimaryKeys") { + TableRef table_ref = {std::make_optional(FLAGS_catalog), + std::make_optional(FLAGS_schema), FLAGS_table}; + ARROW_ASSIGN_OR_RAISE(info, sql_client.GetPrimaryKeys(call_options, table_ref)); + } else if (FLAGS_command == "GetSqlInfo") { + ARROW_ASSIGN_OR_RAISE(info, sql_client.GetSqlInfo(call_options, {})); + } + + if (info != NULLPTR && + !boost::istarts_with(FLAGS_command, "PreparedStatementExecute")) { + return PrintResults(sql_client, call_options, info); + } + + return Status::OK(); +} + +int main(int argc, char** argv) { + gflags::ParseCommandLineFlags(&argc, &argv, true); + + Status st = RunMain(); + if (!st.ok()) { + std::cerr << st << std::endl; + return 1; + } + return 0; +} diff --git a/src/flight_sql.cpp b/src/flight_sql_server.cpp similarity index 100% rename from src/flight_sql.cpp rename to src/flight_sql_server.cpp diff --git a/src/library/include/flight_sql_library.h b/src/library/include/flight_sql_library.h index 4499dd1..ac44e18 100644 --- a/src/library/include/flight_sql_library.h +++ b/src/library/include/flight_sql_library.h @@ -4,7 +4,7 @@ #include // Constants -const std::string FLIGHT_SQL_SERVER_VERSION = "v1.2.0"; // For now - be sure to update this version with the git tag! TODO: automate this +const std::string FLIGHT_SQL_SERVER_VERSION = "v1.2.1"; // For now - be sure to update this version with the git tag! TODO: automate this const std::string DEFAULT_FLIGHT_HOSTNAME = "0.0.0.0"; const std::string DEFAULT_FLIGHT_USERNAME = "flight_username"; const int DEFAULT_FLIGHT_PORT = 31337;