提交 8c8b2176 authored 作者: Chris Rienzo's avatar Chris Rienzo

mod_http_cache: added native Amazon S3 support

上级 fe000f18
BASE=../../../..
LOCAL_OBJS= \
aws.o
LOCAL_SOURCES= \
aws.c
include $(BASE)/build/modmake.rules
/*
* aws.c for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
* Copyright (C) 2013, Grasshopper
*
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is aws.c for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
*
* The Initial Developer of the Original Code is Grasshopper
* Portions created by the Initial Developer are Copyright (C)
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Chris Rienzo <chris.rienzo@grasshopper.com>
*
* aws.c -- Some Amazon Web Services helper functions
*
*/
#include "aws.h"
#include <switch.h>
#if defined(HAVE_OPENSSL)
#include <openssl/hmac.h>
#include <openssl/sha.h>
#endif
/* 160 bits / 8 bits per byte */
#define SHA1_LENGTH 20
/**
* @param url to check
* @return true if this is an S3 url
*/
int aws_s3_is_s3_url(const char *url)
{
/* AWS bucket naming rules are complex... this match only supports virtual hosting of buckets */
return !zstr(url) && switch_regex_match(url, "^https?://[a-z0-9][-a-z0-9.]{1,61}[a-z0-9]\\.s3\\.amazonaws\\.com/.*$") == SWITCH_STATUS_SUCCESS;
}
/**
* Create the string to sign for a AWS signature calculation
* @param verb (PUT/GET)
* @param bucket bucket object is stored in
* @param object to access (filename.ext)
* @param content_type optional content type
* @param content_md5 optional content MD5 checksum
* @param date header
* @return the string_to_sign (must be freed)
*/
char *aws_s3_string_to_sign(const char *verb, const char *bucket, const char *object, const char *content_type, const char *content_md5, const char *date)
{
/*
* String to sign has the following format:
* <HTTP-VERB>\n<Content-MD5>\n<Content-Type>\n<Expires/Date>\n/bucket/object
*/
return switch_mprintf("%s\n%s\n%s\n%s\n/%s/%s",
verb, content_md5 ? content_md5 : "", content_type ? content_type : "",
date, bucket, object);
}
/**
* Create the AWS S3 signature
* @param signature buffer to store the signature
* @param signature_length length of signature buffer
* @param string_to_sign
* @param aws_secret_access_key secret access key
* @return the signature buffer or NULL if missing input
*/
char *aws_s3_signature(char *signature, int signature_length, const char *string_to_sign, const char *aws_secret_access_key)
{
#if defined(HAVE_OPENSSL)
unsigned int signature_raw_length = SHA1_LENGTH;
char signature_raw[SHA1_LENGTH];
signature_raw[0] = '\0';
if (!signature || signature_length <= 0) {
return NULL;
}
if (zstr(aws_secret_access_key)) {
return NULL;
}
if (!string_to_sign) {
string_to_sign = "";
}
HMAC(EVP_sha1(),
aws_secret_access_key,
strlen(aws_secret_access_key),
(const unsigned char *)string_to_sign,
strlen(string_to_sign),
(unsigned char *)signature_raw,
&signature_raw_length);
/* convert result to base64 */
memset(signature, 0, signature_length);
switch_b64_encode((unsigned char *)signature_raw, signature_raw_length, (unsigned char *)signature, signature_length);
#endif
return signature;
}
/**
* Parse bucket and object from URL
* @param url to parse. This value is modified.
* @param bucket to store result in
* @param bucket_length of result buffer
*/
void aws_s3_parse_url(char *url, char **bucket, char **object)
{
char *bucket_start;
char *bucket_end;
char *object_start;
*bucket = NULL;
*object = NULL;
if (!aws_s3_is_s3_url(url)) {
return;
}
/* expect: http(s)://bucket.s3.amazonaws.com/object */
bucket_start = strstr(url, "://");
if (!bucket_start) {
/* invalid URL */
return;
}
bucket_start += 3;
bucket_end = strchr(bucket_start, '.');
if (!bucket_end) {
/* invalid URL */
return;
}
*bucket_end = '\0';
object_start = strchr(bucket_end + 1, '/');
if (!object_start) {
/* invalid URL */
return;
}
object_start++;
if (strchr(object_start, '/')) {
/* invalid URL */
return;
}
if (zstr(bucket_start) || zstr(object_start)) {
/* invalid URL */
return;
}
*bucket = bucket_start;
*object = object_start;
}
/**
* Create a pre-signed URL for AWS S3
* @param verb (PUT/GET)
* @param url address (virtual-host-style)
* @param content_type optional content type
* @param content_md5 optional content MD5 checksum
* @param aws_access_key_id secret access key identifier
* @param aws_secret_access_key secret access key
* @param expires seconds since the epoch
* @return presigned_url
*/
char *aws_s3_presigned_url_create(const char *verb, const char *url, const char *content_type, const char *content_md5, const char *aws_access_key_id, const char *aws_secret_access_key, const char *expires)
{
char signature[S3_SIGNATURE_LENGTH_MAX];
char signature_url_encoded[S3_SIGNATURE_LENGTH_MAX];
char *string_to_sign;
char *url_dup = strdup(url);
char *bucket;
char *object;
/* create URL encoded signature */
aws_s3_parse_url(url_dup, &bucket, &object);
string_to_sign = aws_s3_string_to_sign(verb, bucket, object, content_type, content_md5, expires);
signature[0] = '\0';
aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, string_to_sign, aws_secret_access_key);
switch_url_encode(signature, signature_url_encoded, S3_SIGNATURE_LENGTH_MAX);
free(string_to_sign);
free(url_dup);
/* create the presigned URL */
return switch_mprintf("%s?Signature=%s&Expires=%s&AWSAccessKeyId=%s", url, signature_url_encoded, expires, aws_access_key_id);
}
/**
* Create an authentication signature for AWS S3
* @param authentication buffer to store result
* @param authentication_length maximum result length
* @param verb (PUT/GET)
* @param url address (virtual-host-style)
* @param content_type optional content type
* @param content_md5 optional content MD5 checksum
* @param aws_access_key_id secret access key identifier
* @param aws_secret_access_key secret access key
* @param date header
* @return signature for Authorization header
*/
char *aws_s3_authentication_create(const char *verb, const char *url, const char *content_type, const char *content_md5, const char *aws_access_key_id, const char *aws_secret_access_key, const char *date)
{
char signature[S3_SIGNATURE_LENGTH_MAX];
char *string_to_sign;
char *url_dup = strdup(url);
char *bucket;
char *object;
/* create base64 encoded signature */
aws_s3_parse_url(url_dup, &bucket, &object);
string_to_sign = aws_s3_string_to_sign(verb, bucket, object, content_type, content_md5, date);
signature[0] = '\0';
aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, string_to_sign, aws_secret_access_key);
free(string_to_sign);
free(url_dup);
return switch_mprintf("AWS %s:%s", aws_access_key_id, signature);
}
/* For Emacs:
* Local Variables:
* mode:c
* indent-tabs-mode:t
* tab-width:4
* c-basic-offset:4
* End:
* For VIM:
* vim:set softtabstop=4 shiftwidth=4 tabstop=4
*/
/*
* aws.h for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
* Copyright (C) 2013, Grasshopper
*
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is aws.h for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
*
* The Initial Developer of the Original Code is Grasshopper
* Portions created by the Initial Developer are Copyright (C)
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Chris Rienzo <chris.rienzo@grasshopper.com>
*
* aws.h - Some Amazon Web Services helper functions
*
*/
#ifndef AWS_H
#define AWS_H
#include <switch.h>
/* (SHA1_LENGTH * 1.37 base64 bytes per byte * 3 url-encoded bytes per byte) */
#define S3_SIGNATURE_LENGTH_MAX 83
int aws_s3_is_s3_url(const char *url);
void aws_s3_parse_url(char *url, char **bucket, char **object);
char *aws_s3_string_to_sign(const char *verb, const char *bucket, const char *object, const char *content_type, const char *content_md5, const char *date);
char *aws_s3_signature(char *signature, int signature_length, const char *string_to_sign, const char *aws_secret_access_key);
char *aws_s3_presigned_url_create(const char *verb, const char *url, const char *content_type, const char *content_md5, const char *aws_access_key_id, const char *aws_secret_access_key, const char *expires);
char *aws_s3_authentication_create(const char *verb, const char *url, const char *content_type, const char *content_md5, const char *aws_access_key_id, const char *aws_secret_access_key, const char *date);
#endif
/* For Emacs:
* Local Variables:
* mode:c
* indent-tabs-mode:t
* tab-width:4
* c-basic-offset:4
* End:
* For VIM:
* vim:set softtabstop=4 shiftwidth=4 tabstop=4
*/
......@@ -9,4 +9,19 @@
<param name="ssl-verifyhost" value="true"/>
<param name="ssl-verifypeer" value="true"/>
</settings>
<profiles>
<profile name="s3">
<!-- Credentials for AWS account. Reference by name when sending HTTP request -->
<!-- http_get {profile=s3}http://bucket.s3.amazonaws.com/object.wav -->
<!-- http_put {profile=s3}http://bucket.s3.amazonaws.com/object.wav /tmp/file.wav -->
<aws-s3>
<!-- 20 character key identifier -->
<access-key-id><![CDATA[AKIAIOSFODNN7EXAMPLE]]></access-key-id>
<!-- 40 character secret -->
<secret-access-key><![CDATA[wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY]]></secret-access-key>
<aws-s3>
</profile>
</profiles>
</configuration>
BASE=../../../../..
LOCAL_CFLAGS += -I../ -I./
LOCAL_OBJS= main.o ../aws.o
LOCAL_SOURCES= main.c
include $(BASE)/build/modmake.rules
local_all:
libtool --mode=link gcc main.o ../aws.o -o test test_aws.la
local_clean:
-rm test
#include <switch.h>
#include "test.h"
#include "aws.h"
/**
* Test string to sign generation
*/
static void test_string_to_sign(void)
{
ASSERT_STRING_EQUALS("GET\n\n\nFri, 17 May 2013 19:35:26 GMT\n/rienzo-vault/troporocks.mp3", aws_s3_string_to_sign("GET", "rienzo-vault", "troporocks.mp3", "", "", "Fri, 17 May 2013 19:35:26 GMT"));
ASSERT_STRING_EQUALS("GET\nc8fdb181845a4ca6b8fec737b3581d76\naudio/mpeg\nThu, 17 Nov 2005 18:49:58 GMT\n/foo/man.chu", aws_s3_string_to_sign("GET", "foo", "man.chu", "audio/mpeg", "c8fdb181845a4ca6b8fec737b3581d76", "Thu, 17 Nov 2005 18:49:58 GMT"));
ASSERT_STRING_EQUALS("\n\n\n\n//", aws_s3_string_to_sign("", "", "", "", "", ""));
ASSERT_STRING_EQUALS("\n\n\n\n//", aws_s3_string_to_sign(NULL, NULL, NULL, NULL, NULL, NULL));
}
/**
* Test signature generation
*/
static void test_signature(void)
{
char signature[S3_SIGNATURE_LENGTH_MAX];
signature[0] = '\0';
ASSERT_STRING_EQUALS("weGrLrc9HDlkYPTepVl0A9VYNlw=", aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "GET\n\n\nFri, 17 May 2013 19:35:26 GMT\n/rienzo-vault/troporocks.mp3", "hOIZt1oeTX1JzINOMBoKf0BxONRZNQT1J8gIznLx"));
ASSERT_STRING_EQUALS("jZNOcbfWmD/A/f3hSvVzXZjM2HU=", aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "PUT\nc8fdb181845a4ca6b8fec737b3581d76\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\nx-amz-meta-author:foo@bar.com\n/quotes/nelson", "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV"));
ASSERT_STRING_EQUALS("5m+HAmc5JsrgyDelh9+a2dNrzN8=", aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "GET\n\n\n\nx-amz-date:Thu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\n/quotes/nelson", "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV"));
ASSERT_STRING_EQUALS("OKA87rVp3c4kd59t8D3diFmTfuo=", aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "", "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV"));
ASSERT_STRING_EQUALS("OKA87rVp3c4kd59t8D3diFmTfuo=", aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, NULL, "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV"));
ASSERT_NULL(aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "GET\n\n\n\nx-amz-date:Thu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\n/quotes/nelson", ""));
ASSERT_NULL(aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "", ""));
ASSERT_NULL(aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, NULL, NULL));
ASSERT_NULL(aws_s3_signature(NULL, S3_SIGNATURE_LENGTH_MAX, "PUT\nc8fdb181845a4ca6b8fec737b3581d76\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\nx-amz-meta-author:foo@bar.com\n/quotes/nelson", "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV"));
ASSERT_NULL(aws_s3_signature(signature, 0, "PUT\nc8fdb181845a4ca6b8fec737b3581d76\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\nx-amz-meta-author:foo@bar.com\n/quotes/nelson", "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV"));
/* TODO freeswitch bug... */
//ASSERT_STRING_EQUALS("jZNO", aws_s3_signature(signature, 5, "PUT\nc8fdb181845a4ca6b8fec737b3581d76\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\nx-amz-meta-author:foo@bar.com\n/quotes/nelson", "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV"));
}
/**
* Test amazon URL detection
*/
static void test_check_url(void)
{
ASSERT_TRUE(aws_s3_is_s3_url("http://bucket.s3.amazonaws.com/object.ext"));
ASSERT_TRUE(aws_s3_is_s3_url("http://bucket.s3.amazonaws.com/object"));
ASSERT_TRUE(aws_s3_is_s3_url("http://red.bucket.s3.amazonaws.com/object.ext"));
ASSERT_TRUE(aws_s3_is_s3_url("https://bucket.s3.amazonaws.com/object.ext"));
ASSERT_TRUE(aws_s3_is_s3_url("https://bucket.s3.amazonaws.com/object"));
ASSERT_FALSE(aws_s3_is_s3_url("bucket.s3.amazonaws.com/object.ext"));
ASSERT_FALSE(aws_s3_is_s3_url("https://s3.amazonaws.com/bucket/object"));
ASSERT_FALSE(aws_s3_is_s3_url("http://s3.amazonaws.com/bucket/object"));
ASSERT_FALSE(aws_s3_is_s3_url("http://google.com/"));
ASSERT_FALSE(aws_s3_is_s3_url("http://phono.com/audio/troporocks.mp3"));
ASSERT_FALSE(aws_s3_is_s3_url(""));
ASSERT_FALSE(aws_s3_is_s3_url(NULL));
}
/**
* Test bucket/object extraction from URL
*/
static void test_parse_url(void)
{
char *bucket;
char *object;
aws_s3_parse_url(strdup("http://quotes.s3.amazonaws.com/nelson"), &bucket, &object);
ASSERT_STRING_EQUALS("quotes", bucket);
ASSERT_STRING_EQUALS("nelson", object);
aws_s3_parse_url(strdup("https://quotes.s3.amazonaws.com/nelson.mp3"), &bucket, &object);
ASSERT_STRING_EQUALS("quotes", bucket);
ASSERT_STRING_EQUALS("nelson.mp3", object);
aws_s3_parse_url(strdup("http://s3.amazonaws.com/quotes/nelson"), &bucket, &object);
ASSERT_NULL(bucket);
ASSERT_NULL(object);
aws_s3_parse_url(strdup("http://quotes/quotes/nelson"), &bucket, &object);
ASSERT_NULL(bucket);
ASSERT_NULL(object);
aws_s3_parse_url(strdup("http://quotes.s3.amazonaws.com/"), &bucket, &object);
ASSERT_NULL(bucket);
ASSERT_NULL(object);
aws_s3_parse_url(strdup("http://quotes.s3.amazonaws.com"), &bucket, &object);
ASSERT_NULL(bucket);
ASSERT_NULL(object);
aws_s3_parse_url(strdup("http://quotes"), &bucket, &object);
ASSERT_NULL(bucket);
ASSERT_NULL(object);
aws_s3_parse_url(strdup(""), &bucket, &object);
ASSERT_NULL(bucket);
ASSERT_NULL(object);
aws_s3_parse_url(NULL, &bucket, &object);
ASSERT_NULL(bucket);
ASSERT_NULL(object);
}
/**
* Test Authorization header creation
*/
static void test_authorization_header(void)
{
ASSERT_STRING_EQUALS("AWS AKIAIOSFODNN7EXAMPLE:YJkomOaqUJlvEluDq4fpusID38Y=", aws_s3_authentication_create("GET", "https://vault.s3.amazonaws.com/awesome.mp3", "audio/mpeg", "", "AKIAIOSFODNN7EXAMPLE", "0123456789012345678901234567890123456789", "1234567890"));
}
/**
* Test pre-signed URL creation
*/
static void test_presigned_url(void)
{
ASSERT_STRING_EQUALS("https://vault.s3.amazonaws.com/awesome.mp3?Signature=YJkomOaqUJlvEluDq4fpusID38Y%3D&Expires=1234567890&AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE", aws_s3_presigned_url_create("GET", "https://vault.s3.amazonaws.com/awesome.mp3", "audio/mpeg", "", "AKIAIOSFODNN7EXAMPLE", "0123456789012345678901234567890123456789", "1234567890"));
}
/**
* main program
*/
int main(int argc, char **argv)
{
TEST_INIT
TEST(test_string_to_sign);
TEST(test_signature);
TEST(test_check_url);
TEST(test_parse_url);
TEST(test_authorization_header);
TEST(test_presigned_url);
return 0;
}
/*
* test.h for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
* Copyright (C) 2013, Grasshopper
*
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is test.h for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
*
* The Initial Developer of the Original Code is Grasshopper
* Portions created by the Initial Developer are Copyright (C)
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Chris Rienzo <chris.rienzo@grasshopper.com>
*
* test.h -- simple unit testing macros
*
*/
#ifndef TEST_H
#define TEST_H
#define assert_equals(test, expected_str, expected, actual, file, line) \
{ \
int actual_val = actual; \
if (expected != actual_val) { \
printf("TEST\t%s\tFAIL\t%s\t%i\t!=\t%i\t%s:%i\n", test, expected_str, expected, actual_val, file, line); \
exit(1); \
} else { \
printf("TEST\t%s\tPASS\n", test); \
} \
}
#define assert_string_equals(test, expected, actual, file, line) \
{ \
const char *actual_str = actual; \
if (!actual_str || strcmp(expected, actual_str)) { \
printf("TEST\t%s\tFAIL\t\t%s\t!=\t%s\t%s:%i\n", test, expected, actual_str, file, line); \
exit(1); \
} else { \
printf("TEST\t%s\tPASS\n", test); \
} \
}
#define assert_not_null(test, actual, file, line) \
{ \
const void *actual_val = actual; \
if (!actual_val) { \
printf("TEST\t%s\tFAIL\t\t\t\t\t%s:%i\n", test, file, line); \
exit(1); \
} else { \
printf("TEST\t%s\tPASS\n", test); \
} \
}
#define assert_null(test, actual, file, line) \
{ \
const void *actual_val = actual; \
if (actual_val) { \
printf("TEST\t%s\tFAIL\t\t\t\t\t%s:%i\n", test, file, line); \
exit(1); \
} else { \
printf("TEST\t%s\tPASS\n", test); \
} \
}
#define assert_true(test, actual, file, line) \
{ \
int actual_val = actual; \
if (!actual_val) { \
printf("TEST\t%s\tFAIL\t\t\t\t\t%s:%i\n", test, file, line); \
exit(1); \
} else { \
printf("TEST\t%s\tPASS\n", test); \
} \
}
#define assert_false(test, actual, file, line) \
{ \
int actual_val = actual; \
if (actual_val) { \
printf("TEST\t%s\tFAIL\t\t\t\t\t%s:%i\n", test, file, line); \
exit(1); \
} else { \
printf("TEST\t%s\tPASS\n", test); \
} \
}
#define ASSERT_EQUALS(expected, actual) assert_equals(#actual, #expected, expected, actual, __FILE__, __LINE__)
#define ASSERT_STRING_EQUALS(expected, actual) assert_string_equals(#actual, expected, actual, __FILE__, __LINE__)
#define ASSERT_NOT_NULL(actual) assert_not_null(#actual " not null", actual, __FILE__, __LINE__)
#define ASSERT_NULL(actual) assert_null(#actual " is null", actual, __FILE__, __LINE__)
#define ASSERT_TRUE(actual) assert_true(#actual " is true", actual, __FILE__, __LINE__)
#define ASSERT_FALSE(actual) assert_false(#actual " is false", actual, __FILE__, __LINE__)
#define SKIP_ASSERT_EQUALS(expected, actual) if (0) { ASSERT_EQUALS(expected, actual); }
#define TEST(name) printf("TEST BEGIN\t" #name "\n"); name(); printf("TEST END\t"#name "\tPASS\n");
#define SKIP_TEST(name) if (0) { TEST(name) };
#define TEST_INIT const char *err; switch_core_init(0, SWITCH_TRUE, &err);
#endif
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论