Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
L
libvmod-blobdigest
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
2
Issues
2
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
uplex-varnish
libvmod-blobdigest
Commits
385e578c
Commit
385e578c
authored
Feb 14, 2016
by
Geoff Simmons
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add SHA1
parent
c2a72457
Changes
8
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
419 additions
and
2 deletions
+419
-2
Makefile.am
src/Makefile.am
+2
-0
gen_enum_parse.pl
src/gen_enum_parse.pl
+1
-0
sha1.c
src/sha1.c
+196
-0
sha1.h
src/sha1.h
+25
-0
sha1.vtc
src/tests/sha1.vtc
+177
-0
vmod_blobdigest.c
src/vmod_blobdigest.c
+9
-0
vmod_blobdigest.h
src/vmod_blobdigest.h
+6
-0
vmod_blobdigest.vcc
src/vmod_blobdigest.vcc
+3
-2
No files found.
src/Makefile.am
View file @
385e578c
...
@@ -12,6 +12,8 @@ libvmod_blobdigest_la_SOURCES = \
...
@@ -12,6 +12,8 @@ libvmod_blobdigest_la_SOURCES = \
byte_order.c
\
byte_order.c
\
md5.h
\
md5.h
\
md5.c
\
md5.c
\
sha1.h
\
sha1.c
\
sha256.h
\
sha256.h
\
sha256.c
\
sha256.c
\
sha512.h
\
sha512.h
\
...
...
src/gen_enum_parse.pl
View file @
385e578c
...
@@ -5,6 +5,7 @@ use warnings;
...
@@ -5,6 +5,7 @@ use warnings;
my
@vals
=
(
qw(
my
@vals
=
(
qw(
MD5
MD5
SHA1
SHA224
SHA224
SHA256
SHA256
SHA384
SHA384
...
...
src/sha1.c
0 → 100644
View file @
385e578c
/* sha1.c - an implementation of Secure Hash Algorithm 1 (SHA1)
* based on RFC 3174.
*
* Copyright: 2008-2012 Aleksey Kravchenko <rhash.admin@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. Use this program at your own risk!
*/
#include <string.h>
#include "byte_order.h"
#include "sha1.h"
/**
* Initialize context before calculaing hash.
*
* @param ctx context to initialize
*/
void
rhash_sha1_init
(
sha1_ctx
*
ctx
)
{
ctx
->
length
=
0
;
/* initialize algorithm state */
ctx
->
hash
[
0
]
=
0x67452301
;
ctx
->
hash
[
1
]
=
0xefcdab89
;
ctx
->
hash
[
2
]
=
0x98badcfe
;
ctx
->
hash
[
3
]
=
0x10325476
;
ctx
->
hash
[
4
]
=
0xc3d2e1f0
;
}
/**
* The core transformation. Process a 512-bit block.
* The function has been taken from RFC 3174 with little changes.
*
* @param hash algorithm state
* @param block the message block to process
*/
static
void
rhash_sha1_process_block
(
unsigned
*
hash
,
const
unsigned
*
block
)
{
int
t
;
/* Loop counter */
uint32_t
temp
;
/* Temporary word value */
uint32_t
W
[
80
];
/* Word sequence */
uint32_t
A
,
B
,
C
,
D
,
E
;
/* Word buffers */
/* initialize the first 16 words in the array W */
for
(
t
=
0
;
t
<
16
;
t
++
)
{
/* note: it is much faster to apply be2me here, then using be32_copy */
W
[
t
]
=
be2me_32
(
block
[
t
]);
}
/* initialize the rest */
for
(
t
=
16
;
t
<
80
;
t
++
)
{
W
[
t
]
=
ROTL32
(
W
[
t
-
3
]
^
W
[
t
-
8
]
^
W
[
t
-
14
]
^
W
[
t
-
16
],
1
);
}
A
=
hash
[
0
];
B
=
hash
[
1
];
C
=
hash
[
2
];
D
=
hash
[
3
];
E
=
hash
[
4
];
for
(
t
=
0
;
t
<
20
;
t
++
)
{
/* the following is faster than ((B & C) | ((~B) & D)) */
temp
=
ROTL32
(
A
,
5
)
+
(((
C
^
D
)
&
B
)
^
D
)
+
E
+
W
[
t
]
+
0x5A827999
;
E
=
D
;
D
=
C
;
C
=
ROTL32
(
B
,
30
);
B
=
A
;
A
=
temp
;
}
for
(
t
=
20
;
t
<
40
;
t
++
)
{
temp
=
ROTL32
(
A
,
5
)
+
(
B
^
C
^
D
)
+
E
+
W
[
t
]
+
0x6ED9EBA1
;
E
=
D
;
D
=
C
;
C
=
ROTL32
(
B
,
30
);
B
=
A
;
A
=
temp
;
}
for
(
t
=
40
;
t
<
60
;
t
++
)
{
temp
=
ROTL32
(
A
,
5
)
+
((
B
&
C
)
|
(
B
&
D
)
|
(
C
&
D
))
+
E
+
W
[
t
]
+
0x8F1BBCDC
;
E
=
D
;
D
=
C
;
C
=
ROTL32
(
B
,
30
);
B
=
A
;
A
=
temp
;
}
for
(
t
=
60
;
t
<
80
;
t
++
)
{
temp
=
ROTL32
(
A
,
5
)
+
(
B
^
C
^
D
)
+
E
+
W
[
t
]
+
0xCA62C1D6
;
E
=
D
;
D
=
C
;
C
=
ROTL32
(
B
,
30
);
B
=
A
;
A
=
temp
;
}
hash
[
0
]
+=
A
;
hash
[
1
]
+=
B
;
hash
[
2
]
+=
C
;
hash
[
3
]
+=
D
;
hash
[
4
]
+=
E
;
}
/**
* Calculate message hash.
* Can be called repeatedly with chunks of the message to be hashed.
*
* @param ctx the algorithm context containing current hashing state
* @param msg message chunk
* @param size length of the message chunk
*/
void
rhash_sha1_update
(
sha1_ctx
*
ctx
,
const
unsigned
char
*
msg
,
size_t
size
)
{
unsigned
index
=
(
unsigned
)
ctx
->
length
&
63
;
ctx
->
length
+=
size
;
/* fill partial block */
if
(
index
)
{
unsigned
left
=
sha1_block_size
-
index
;
memcpy
(
ctx
->
message
+
index
,
msg
,
(
size
<
left
?
size
:
left
));
if
(
size
<
left
)
return
;
/* process partial block */
rhash_sha1_process_block
(
ctx
->
hash
,
(
unsigned
*
)
ctx
->
message
);
msg
+=
left
;
size
-=
left
;
}
while
(
size
>=
sha1_block_size
)
{
unsigned
*
aligned_message_block
;
if
(
IS_ALIGNED_32
(
msg
))
{
/* the most common case is processing of an already aligned message
without copying it */
aligned_message_block
=
(
unsigned
*
)
msg
;
}
else
{
memcpy
(
ctx
->
message
,
msg
,
sha1_block_size
);
aligned_message_block
=
(
unsigned
*
)
ctx
->
message
;
}
rhash_sha1_process_block
(
ctx
->
hash
,
aligned_message_block
);
msg
+=
sha1_block_size
;
size
-=
sha1_block_size
;
}
if
(
size
)
{
/* save leftovers */
memcpy
(
ctx
->
message
,
msg
,
size
);
}
}
/**
* Store calculated hash into the given array.
*
* @param ctx the algorithm context containing current hashing state
* @param result calculated hash in binary form
*/
void
rhash_sha1_final
(
sha1_ctx
*
ctx
,
unsigned
char
*
result
)
{
unsigned
index
=
(
unsigned
)
ctx
->
length
&
63
;
unsigned
*
msg32
=
(
unsigned
*
)
ctx
->
message
;
/* pad message and run for last block */
ctx
->
message
[
index
++
]
=
0x80
;
while
((
index
&
3
)
!=
0
)
{
ctx
->
message
[
index
++
]
=
0
;
}
index
>>=
2
;
/* if no room left in the message to store 64-bit message length */
if
(
index
>
14
)
{
/* then fill the rest with zeros and process it */
while
(
index
<
16
)
{
msg32
[
index
++
]
=
0
;
}
rhash_sha1_process_block
(
ctx
->
hash
,
msg32
);
index
=
0
;
}
while
(
index
<
14
)
{
msg32
[
index
++
]
=
0
;
}
msg32
[
14
]
=
be2me_32
(
(
unsigned
)(
ctx
->
length
>>
29
)
);
msg32
[
15
]
=
be2me_32
(
(
unsigned
)(
ctx
->
length
<<
3
)
);
rhash_sha1_process_block
(
ctx
->
hash
,
msg32
);
if
(
result
)
be32_copy
(
result
,
0
,
&
ctx
->
hash
,
sha1_hash_size
);
}
src/sha1.h
0 → 100644
View file @
385e578c
/* sha1.h */
#ifndef SHA1_H
#define SHA1_H
#include <stdint.h>
#include <unistd.h>
#define sha1_block_size 64
#define sha1_hash_size 20
/* algorithm context */
typedef
struct
sha1_ctx
{
unsigned
char
message
[
sha1_block_size
];
/* 512-bit buffer for leftovers */
uint64_t
length
;
/* number of processed bytes */
unsigned
hash
[
5
];
/* 160-bit algorithm internal hashing state */
}
sha1_ctx
;
/* hash functions */
void
rhash_sha1_init
(
sha1_ctx
*
ctx
);
void
rhash_sha1_update
(
sha1_ctx
*
ctx
,
const
unsigned
char
*
msg
,
size_t
size
);
void
rhash_sha1_final
(
sha1_ctx
*
ctx
,
unsigned
char
*
result
);
#endif
/* SHA1_H */
src/tests/sha1.vtc
0 → 100644
View file @
385e578c
# looks like -*- vcl -*-
varnishtest "SHA1 hash"
# VMOD blobcode must be installed
varnish v1 -vcl {
import blobdigest from "${vmod_topbuild}/src/.libs/libvmod_blobdigest.so";
import blobcode;
backend b { .host = "${bad_ip}"; }
sub vcl_init {
# RFC2202 test cases
new k1 = blobcode.blob(HEX,
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
new rfc2202t1 = blobdigest.hmac(SHA1, k1.get());
new k2 = blobcode.blob(IDENTITY, "Jefe");
new rfc2202t2 = blobdigest.hmac(SHA1, k2.get());
new k3 = blobcode.blob(HEX,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
new rfc2202t3 = blobdigest.hmac(SHA1, k3.get());
new k4 = blobcode.blob(HEX,
"0102030405060708090a0b0c0d0e0f10111213141516171819");
new rfc2202t4 = blobdigest.hmac(SHA1, k4.get());
new k5 = blobcode.blob(HEX,
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c");
new rfc2202t5 = blobdigest.hmac(SHA1, k5.get());
new k6 = blobcode.blob(HEX,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
new rfc2202t6 = blobdigest.hmac(SHA1, k6.get());
}
sub vcl_recv {
return(synth(200));
}
sub vcl_synth {
set resp.http.empty
= blobcode.encode(HEXUC, blobdigest.hash(SHA1,
blobcode.decode(IDENTITY, "")));
set resp.http.emptyb64
= blobcode.encode(BASE64, blobdigest.hash(SHA1,
blobcode.decode(IDENTITY, "")));
set resp.http.a
= blobcode.encode(HEXUC, blobdigest.hash(SHA1,
blobcode.decode(IDENTITY, "a")));
set resp.http.abc
= blobcode.encode(HEXUC, blobdigest.hash(SHA1,
blobcode.decode(IDENTITY, "abc")));
set resp.http.msgdigest
= blobcode.encode(HEXUC, blobdigest.hash(SHA1,
blobcode.decode(IDENTITY,
"message digest")));
set resp.http.alphasoup
= blobcode.encode(HEXUC, blobdigest.hash(SHA1,
blobcode.decode(IDENTITY,
"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")));
set resp.http.alphanum
= blobcode.encode(HEXUC, blobdigest.hash(SHA1,
blobcode.decode(IDENTITY,
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")));
set resp.http.digits
= blobcode.encode(HEXUC, blobdigest.hash(SHA1,
blobcode.decode(IDENTITY,
"12345678901234567890123456789012345678901234567890123456789012345678901234567890")));
set resp.http.pangram
= blobcode.encode(HEXUC, blobdigest.hash(SHA1,
blobcode.decode(IDENTITY,
"The quick brown fox jumps over the lazy dog")));
set resp.http.pangramcog
= blobcode.encode(HEXLC, blobdigest.hash(SHA1,
blobcode.decode(IDENTITY,
"The quick brown fox jumps over the lazy cog")));
set resp.http.pangramb64
= blobcode.encode(BASE64, blobdigest.hash(SHA1,
blobcode.decode(IDENTITY,
"The quick brown fox jumps over the lazy dog")));
set resp.http.pangramcogb64
= blobcode.encode(BASE64, blobdigest.hash(SHA1,
blobcode.decode(IDENTITY,
"The quick brown fox jumps over the lazy cog")));
# all 256 byte values in ascending, big-endian order
set resp.http.allbytes
= blobcode.encode(HEXLC, blobdigest.hash(SHA1,
blobcode.decode(BASE64,
"AQACAQMCBAMFBAYFBwYIBwkICgkLCgwLDQwODQ8OEA8REBIRExIUExUUFhUXFhgXGRgaGRsaHBsdHB4dHx4gHyEgIiEjIiQjJSQmJScmKCcpKCopKyosKy0sLi0vLjAvMTAyMTMyNDM1NDY1NzY4Nzk4Ojk7Ojw7PTw+PT8+QD9BQEJBQ0JEQ0VERkVHRkhHSUhKSUtKTEtNTE5NT05QT1FQUlFTUlRTVVRWVVdWWFdZWFpZW1pcW11cXl1fXmBfYWBiYWNiZGNlZGZlZ2ZoZ2loamlramxrbWxubW9ucG9xcHJxc3J0c3V0dnV3dnh3eXh6eXt6fHt9fH59f36Afw==")));
set resp.http.rfc2202t1 = blobcode.encode(HEXLC,
rfc2202t1.hmac(blobcode.decode(IDENTITY, "Hi There")));
set resp.http.rfc2202t2
= blobcode.encode(HEXLC,
rfc2202t2.hmac(blobcode.decode(IDENTITY,
"what do ya want for nothing?")));
set resp.http.rfc2202t3
= blobcode.encode(HEXLC,
rfc2202t3.hmac(blobcode.decode(HEX,
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd")));
set resp.http.rfc2202t4
= blobcode.encode(HEXLC,
rfc2202t4.hmac(blobcode.decode(HEX,
"cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd")));
set resp.http.rfc2202t5
= blobcode.encode(HEXLC,
rfc2202t5.hmac(blobcode.decode(IDENTITY,
"Test With Truncation")));
set resp.http.rfc2202t6
= blobcode.encode(HEXLC,
rfc2202t6.hmac(blobcode.decode(IDENTITY,
"Test Using Larger Than Block-Size Key - Hash Key First")));
/*
* Test case 7 uses the same key as 6, so we'll re-use
* object rfc2202t6. This tests repeated use of the same
* internal hash contexts.
*/
set resp.http.rfc2202t7
= blobcode.encode(HEXLC,
rfc2202t6.hmac(blobcode.decode(IDENTITY,
"Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data")));
}
} -start
client c1 {
txreq
rxresp
expect resp.status == 200
# from librhash
expect resp.http.empty == "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"
expect resp.http.a == "86F7E437FAA5A7FCE15D1DDCB9EAEAEA377667B8"
expect resp.http.abc == "A9993E364706816ABA3E25717850C26C9CD0D89D"
expect resp.http.msgdigest == "C12252CEDA8BE8994D5FA0290A47231C1D16AAE3"
expect resp.http.pangram == "2FD4E1C67A2D28FCED849EE1BB76E7391B93EB12"
expect resp.http.alphanum == "761C457BF73B14D27E9E9265C46F4B4DDA11F940"
expect resp.http.digits == "50ABF5706A150990A08B2C5EA40FA0E585554732"
# from Wikipedia
expect resp.http.emptyb64 == "2jmj7l5rSw0yVb/vlWAYkK/YBwk="
expect resp.http.pangramcog == "de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3"
expect resp.http.pangramb64 == "L9ThxnotKPzthJ7hu3bnORuT6xI="
expect resp.http.pangramcogb64 == "3p8sf9JeGzr60+haC9F9mxANtLM="
# verified with: base64 -d | sha1sum
expect resp.http.allbytes == "f70d8ebbcd1f4873eb8ceba93879e8ff60b4e4b3"
# RFC2202 test cases
expect resp.http.rfc2202t1 == "b617318655057264e28bc0b6fb378c8ef146be00"
expect resp.http.rfc2202t2 == "effcdf6ae5eb2fa2d27416d5f184df9c259a7c79"
expect resp.http.rfc2202t3 == "125d7342b9ac11cd91a39af48aa17b4f63f175d3"
expect resp.http.rfc2202t4 == "4c9007f4026250c6bc8414f9bf50c86c2d7235da"
expect resp.http.rfc2202t5 == "4c1a03424b55e07fe7f27be1d58bb9324a9a5a04"
expect resp.http.rfc2202t6 == "aa4ae5e15272d00e95705637ce8a3b55ed402112"
expect resp.http.rfc2202t7 == "e8e99d0f45237d786d6bbaa7965c7808bbff1a91"
} -run
src/vmod_blobdigest.c
View file @
385e578c
...
@@ -84,6 +84,9 @@ init(const enum algorithm hash, hash_ctx * const hctx)
...
@@ -84,6 +84,9 @@ init(const enum algorithm hash, hash_ctx * const hctx)
case
MD5
:
case
MD5
:
rhash_md5_init
(
&
hctx
->
md5
);
rhash_md5_init
(
&
hctx
->
md5
);
break
;
break
;
case
SHA1
:
rhash_sha1_init
(
&
hctx
->
sha1
);
break
;
case
SHA224
:
case
SHA224
:
rhash_sha224_init
(
&
hctx
->
sha224
);
rhash_sha224_init
(
&
hctx
->
sha224
);
break
;
break
;
...
@@ -109,6 +112,9 @@ update(const enum algorithm hash, hash_ctx *restrict const hctx,
...
@@ -109,6 +112,9 @@ update(const enum algorithm hash, hash_ctx *restrict const hctx,
case
MD5
:
case
MD5
:
rhash_md5_update
(
&
hctx
->
md5
,
msg
,
len
);
rhash_md5_update
(
&
hctx
->
md5
,
msg
,
len
);
break
;
break
;
case
SHA1
:
rhash_sha1_update
(
&
hctx
->
sha1
,
msg
,
len
);
break
;
case
SHA224
:
case
SHA224
:
rhash_sha256_update
(
&
hctx
->
sha224
,
msg
,
len
);
rhash_sha256_update
(
&
hctx
->
sha224
,
msg
,
len
);
break
;
break
;
...
@@ -132,6 +138,9 @@ final(const enum algorithm hash, hash_ctx *restrict const hctx,
...
@@ -132,6 +138,9 @@ final(const enum algorithm hash, hash_ctx *restrict const hctx,
case
MD5
:
case
MD5
:
rhash_md5_final
(
&
hctx
->
md5
,
result
);
rhash_md5_final
(
&
hctx
->
md5
,
result
);
break
;
break
;
case
SHA1
:
rhash_sha1_final
(
&
hctx
->
sha1
,
result
);
break
;
case
SHA224
:
case
SHA224
:
rhash_sha256_final
(
&
hctx
->
sha224
,
result
);
rhash_sha256_final
(
&
hctx
->
sha224
,
result
);
break
;
break
;
...
...
src/vmod_blobdigest.h
View file @
385e578c
...
@@ -29,12 +29,14 @@
...
@@ -29,12 +29,14 @@
#include "parse_algorithm.h"
#include "parse_algorithm.h"
#include "md5.h"
#include "md5.h"
#include "sha1.h"
#include "vsha256.h"
#include "vsha256.h"
#include "sha256.h"
#include "sha256.h"
#include "sha512.h"
#include "sha512.h"
typedef
union
hash_ctx
{
typedef
union
hash_ctx
{
md5_ctx
md5
;
md5_ctx
md5
;
sha1_ctx
sha1
;
sha256_ctx
sha224
;
sha256_ctx
sha224
;
SHA256_CTX
sha256
;
SHA256_CTX
sha256
;
sha512_ctx
sha512
;
sha512_ctx
sha512
;
...
@@ -48,6 +50,10 @@ static const struct hashspec {
...
@@ -48,6 +50,10 @@ static const struct hashspec {
md5_hash_size
,
md5_hash_size
,
md5_block_size
,
md5_block_size
,
},
},
[
SHA1
]
=
{
sha1_hash_size
,
sha1_block_size
,
},
[
SHA224
]
=
{
[
SHA224
]
=
{
sha224_hash_size
,
sha224_hash_size
,
sha256_block_size
,
sha256_block_size
,
...
...
src/vmod_blobdigest.vcc
View file @
385e578c
...
@@ -9,7 +9,7 @@
...
@@ -9,7 +9,7 @@
$Module blobdigest 3 digests and hmacs for the VCL blob type
$Module blobdigest 3 digests and hmacs for the VCL blob type
$Object hmac(ENUM {MD5, SHA224, SHA256, SHA384, SHA512} hash, BLOB key)
$Object hmac(ENUM {MD5, SHA
1, SHA
224, SHA256, SHA384, SHA512} hash, BLOB key)
Prototype
Prototype
new OBJ = blobdigest.hmac(ENUM hash, BLOB key)
new OBJ = blobdigest.hmac(ENUM hash, BLOB key)
...
@@ -31,7 +31,8 @@ Description
...
@@ -31,7 +31,8 @@ Description
Example
Example
``set req.http.hmac = hmac.hmac(blobcode.decode(BASE64, "Zm9v"));``
``set req.http.hmac = hmac.hmac(blobcode.decode(BASE64, "Zm9v"));``
$Function BLOB hash(ENUM {MD5, SHA224, SHA256, SHA384, SHA512} hash, BLOB msg)
$Function BLOB hash(ENUM {MD5, SHA1, SHA224, SHA256, SHA384, SHA512} hash,
BLOB msg)
$Function STRING version()
$Function STRING version()
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment