Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
com.ccwangluo.accelerator
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
sheteng
com.ccwangluo.accelerator
Commits
89da9abd
Commit
89da9abd
authored
Dec 19, 2012
by
Max Lv
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refine libexec
parent
5bb9ccd8
Changes
9
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
102 additions
and
109 deletions
+102
-109
AndroidManifest.xml
AndroidManifest.xml
+2
-2
jni/Application.mk
jni/Application.mk
+1
-1
jni/termExec.cpp
jni/termExec.cpp
+38
-25
libs/armeabi/libexec.so
libs/armeabi/libexec.so
+0
-0
pom.xml
pom.xml
+1
-1
src/com/github/shadowsocks/Exec.java
src/com/github/shadowsocks/Exec.java
+7
-5
src/com/github/shadowsocks/Shadowsocks.java
src/com/github/shadowsocks/Shadowsocks.java
+0
-0
src/com/github/shadowsocks/ShadowsocksApplication.java
src/com/github/shadowsocks/ShadowsocksApplication.java
+8
-0
src/com/github/shadowsocks/Utils.java
src/com/github/shadowsocks/Utils.java
+45
-75
No files found.
AndroidManifest.xml
View file @
89da9abd
...
@@ -2,8 +2,8 @@
...
@@ -2,8 +2,8 @@
<manifest
xmlns:android=
"http://schemas.android.com/apk/res/android"
<manifest
xmlns:android=
"http://schemas.android.com/apk/res/android"
package=
"com.github.shadowsocks"
package=
"com.github.shadowsocks"
android:installLocation=
"auto"
android:installLocation=
"auto"
android:versionCode=
"
4
"
android:versionCode=
"
5
"
android:versionName=
"1.1"
>
android:versionName=
"1.1
.1
"
>
<uses-permission
android:name=
"android.permission.INTERNET"
/>
<uses-permission
android:name=
"android.permission.INTERNET"
/>
<uses-permission
android:name=
"android.permission.ACCESS_NETWORK_STATE"
/>
<uses-permission
android:name=
"android.permission.ACCESS_NETWORK_STATE"
/>
...
...
jni/Application.mk
View file @
89da9abd
APP_PLATFORM
=
an
rdoid-8
APP_PLATFORM
=
an
droid-9
jni/termExec.cpp
View file @
89da9abd
...
@@ -49,6 +49,7 @@
...
@@ -49,6 +49,7 @@
#include <unistd.h>
#include <unistd.h>
#include <termios.h>
#include <termios.h>
#include <signal.h>
#include <signal.h>
#include <stdio.h>
static
jclass
class_fileDescriptor
;
static
jclass
class_fileDescriptor
;
static
jfieldID
field_fileDescriptor_descriptor
;
static
jfieldID
field_fileDescriptor_descriptor
;
...
@@ -98,24 +99,15 @@ static int throwOutOfMemoryError(JNIEnv *env, const char *message)
...
@@ -98,24 +99,15 @@ static int throwOutOfMemoryError(JNIEnv *env, const char *message)
return
env
->
ThrowNew
(
exClass
,
message
);
return
env
->
ThrowNew
(
exClass
,
message
);
}
}
static
int
create_subprocess
(
const
char
*
cmd
,
static
int
create_subprocess
(
const
char
*
cmd
,
char
*
const
argv
[],
char
*
const
argv
[],
char
*
const
envp
[]
,
int
*
pProcessId
)
char
*
const
envp
[],
const
char
*
scripts
,
int
*
pProcessId
)
{
{
char
filename
[]
=
"/data/data/com.github.shadowsocks/defout"
;
char
script
[]
=
"/data/data/com.github.shadowsocks/script"
;
pid_t
pid
;
pid_t
pid
;
int
pfds
[
2
];
int
pfds2
[
2
];
if
(
chmod
(
script
,
0755
)
<
0
)
{
pipe
(
pfds
);
LOGE
(
"error to chmod
\n
"
);
pipe
(
pfds2
);
exit
(
-
1
);
}
int
defout
=
open
(
filename
,
O_RDWR
|
O_CREAT
|
O_TRUNC
,
0600
);
if
(
defout
<
0
)
{
LOGE
(
"open file error
\n
"
);
exit
(
-
1
);
}
pid
=
fork
();
pid
=
fork
();
...
@@ -126,28 +118,41 @@ static int create_subprocess(const char *cmd,
...
@@ -126,28 +118,41 @@ static int create_subprocess(const char *cmd,
if
(
pid
==
0
){
if
(
pid
==
0
){
//setsid();
dup2
(
defout
,
1
);
dup2
(
defout
,
2
);
if
(
envp
)
{
if
(
envp
)
{
for
(;
*
envp
;
++
envp
)
{
for
(;
*
envp
;
++
envp
)
{
putenv
(
*
envp
);
putenv
(
*
envp
);
}
}
}
}
dup2
(
pfds
[
0
],
0
);
close
(
pfds
[
1
]);
dup2
(
pfds2
[
1
],
1
);
dup2
(
pfds2
[
1
],
2
);
close
(
pfds2
[
0
]);
execv
(
cmd
,
argv
);
execv
(
cmd
,
argv
);
exit
(
-
1
);
fflush
(
NULL
);
exit
(
0
);
}
else
{
}
else
{
*
pProcessId
=
(
int
)
pid
;
*
pProcessId
=
(
int
)
pid
;
return
defout
;
dup2
(
pfds
[
1
],
1
);
close
(
pfds
[
0
]);
write
(
pfds
[
1
],
scripts
,
strlen
(
scripts
)
+
1
);
close
(
pfds2
[
1
]);
return
pfds2
[
0
];
}
}
}
}
static
jobject
android_os_Exec_createSubProcess
(
JNIEnv
*
env
,
jobject
clazz
,
static
jobject
android_os_Exec_createSubProcess
(
JNIEnv
*
env
,
jobject
clazz
,
jstring
cmd
,
jobjectArray
args
,
jobjectArray
envVars
,
jstring
cmd
,
jobjectArray
args
,
jobjectArray
envVars
,
jstring
scripts
,
jintArray
processIdArray
)
jintArray
processIdArray
)
{
{
const
jchar
*
str
=
cmd
?
env
->
GetStringCritical
(
cmd
,
0
)
:
0
;
const
jchar
*
str
=
cmd
?
env
->
GetStringCritical
(
cmd
,
0
)
:
0
;
...
@@ -157,6 +162,13 @@ static jobject android_os_Exec_createSubProcess(JNIEnv *env, jobject clazz,
...
@@ -157,6 +162,13 @@ static jobject android_os_Exec_createSubProcess(JNIEnv *env, jobject clazz,
env
->
ReleaseStringCritical
(
cmd
,
str
);
env
->
ReleaseStringCritical
(
cmd
,
str
);
}
}
const
jchar
*
str_scripts
=
scripts
?
env
->
GetStringCritical
(
scripts
,
0
)
:
0
;
String8
scripts_8
;
if
(
str_scripts
)
{
scripts_8
.
set
(
str_scripts
,
env
->
GetStringLength
(
scripts
));
env
->
ReleaseStringCritical
(
scripts
,
str_scripts
);
}
jsize
size
=
args
?
env
->
GetArrayLength
(
args
)
:
0
;
jsize
size
=
args
?
env
->
GetArrayLength
(
args
)
:
0
;
char
**
argv
=
NULL
;
char
**
argv
=
NULL
;
String8
tmp_8
;
String8
tmp_8
;
...
@@ -203,7 +215,8 @@ static jobject android_os_Exec_createSubProcess(JNIEnv *env, jobject clazz,
...
@@ -203,7 +215,8 @@ static jobject android_os_Exec_createSubProcess(JNIEnv *env, jobject clazz,
}
}
int
procId
;
int
procId
;
int
ptm
=
create_subprocess
(
cmd_8
.
string
(),
argv
,
envp
,
&
procId
);
int
ptm
=
create_subprocess
(
cmd_8
.
string
(),
argv
,
envp
,
scripts_8
.
string
(),
&
procId
);
if
(
argv
)
{
if
(
argv
)
{
for
(
char
**
tmp
=
argv
;
*
tmp
;
++
tmp
)
{
for
(
char
**
tmp
=
argv
;
*
tmp
;
++
tmp
)
{
...
@@ -311,7 +324,7 @@ static int register_FileDescriptor(JNIEnv *env)
...
@@ -311,7 +324,7 @@ static int register_FileDescriptor(JNIEnv *env)
static
const
char
*
classPathName
=
"com/github/shadowsocks/Exec"
;
static
const
char
*
classPathName
=
"com/github/shadowsocks/Exec"
;
static
JNINativeMethod
method_table
[]
=
{
static
JNINativeMethod
method_table
[]
=
{
{
"createSubprocess"
,
"(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;[I)Ljava/io/FileDescriptor;"
,
{
"createSubprocess"
,
"(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;
Ljava/lang/String;
[I)Ljava/io/FileDescriptor;"
,
(
void
*
)
android_os_Exec_createSubProcess
},
(
void
*
)
android_os_Exec_createSubProcess
},
{
"waitFor"
,
"(I)I"
,
{
"waitFor"
,
"(I)I"
,
(
void
*
)
android_os_Exec_waitFor
},
(
void
*
)
android_os_Exec_waitFor
},
...
...
libs/armeabi/libexec.so
View file @
89da9abd
No preview for this file type
pom.xml
View file @
89da9abd
...
@@ -4,7 +4,7 @@
...
@@ -4,7 +4,7 @@
<modelVersion>
4.0.0
</modelVersion>
<modelVersion>
4.0.0
</modelVersion>
<groupId>
com.github.shadowsocks
</groupId>
<groupId>
com.github.shadowsocks
</groupId>
<artifactId>
shadowsocks
</artifactId>
<artifactId>
shadowsocks
</artifactId>
<version>
1.1
</version>
<version>
1.1
.1
</version>
<packaging>
apk
</packaging>
<packaging>
apk
</packaging>
<name>
Shadowsocks
</name>
<name>
Shadowsocks
</name>
...
...
src/com/github/shadowsocks/Exec.java
View file @
89da9abd
...
@@ -43,16 +43,18 @@ public class Exec {
...
@@ -43,16 +43,18 @@ public class Exec {
* Callers are responsible for calling Exec.close() on the returned file
* Callers are responsible for calling Exec.close() on the returned file
* descriptor.
* descriptor.
*
*
* @param cmd The command to execute
* @param cmd The command to execute
.
* @param args An array of arguments to the command
* @param args An array of arguments to the command
.
* @param envVars An array of strings of the form "VAR=value" to be added to the
* @param envVars An array of strings of the form "VAR=value" to be added to the
* environment of the process
* environment of the process.
* @param scripts The scripts to execute.
* @param processId A one-element array to which the process ID of the started
* @param processId A one-element array to which the process ID of the started
* process will be written.
* process will be written.
* @return
the file descriptor of the started process.
* @return
File descriptor
*/
*/
public
static
native
FileDescriptor
createSubprocess
(
String
cmd
,
public
static
native
FileDescriptor
createSubprocess
(
String
cmd
,
String
[]
args
,
String
[]
envVars
,
int
[]
processId
);
String
[]
args
,
String
[]
envVars
,
String
scripts
,
int
[]
processId
);
/**
/**
* Send SIGHUP to a process group.
* Send SIGHUP to a process group.
...
...
src/com/github/shadowsocks/Shadowsocks.java
100644 → 100755
View file @
89da9abd
File mode changed from 100644 to 100755
src/com/github/shadowsocks/ShadowsocksApplication.java
View file @
89da9abd
package
com.github.shadowsocks
;
package
com.github.shadowsocks
;
import
android.app.Application
;
import
android.app.Application
;
import
android.content.Context
;
import
com.google.analytics.tracking.android.EasyTracker
;
import
com.google.analytics.tracking.android.EasyTracker
;
public
class
ShadowsocksApplication
extends
Application
{
public
class
ShadowsocksApplication
extends
Application
{
private
static
String
sTmpDir
;
public
static
String
getTmpDir
()
{
return
sTmpDir
;
}
@Override
@Override
public
void
onCreate
()
{
public
void
onCreate
()
{
EasyTracker
.
getInstance
().
setContext
(
this
);
EasyTracker
.
getInstance
().
setContext
(
this
);
sTmpDir
=
getCacheDir
().
getAbsolutePath
();
}
}
}
}
src/com/github/shadowsocks/Utils.java
View file @
89da9abd
...
@@ -8,7 +8,10 @@ import android.graphics.drawable.Drawable;
...
@@ -8,7 +8,10 @@ import android.graphics.drawable.Drawable;
import
android.os.Environment
;
import
android.os.Environment
;
import
android.util.Log
;
import
android.util.Log
;
import
java.io.*
;
import
java.io.File
;
import
java.io.FileDescriptor
;
import
java.io.FileInputStream
;
import
java.io.InputStream
;
import
java.util.ArrayList
;
import
java.util.ArrayList
;
public
class
Utils
{
public
class
Utils
{
...
@@ -17,51 +20,32 @@ public class Utils {
...
@@ -17,51 +20,32 @@ public class Utils {
* Internal thread used to execute scripts (as root or not).
* Internal thread used to execute scripts (as root or not).
*/
*/
private
static
final
class
ScriptRunner
extends
Thread
{
private
static
final
class
ScriptRunner
extends
Thread
{
private
final
File
file
;
private
final
String
scripts
;
private
final
String
script
;
private
final
StringBuilder
result
;
private
final
StringBuilder
res
;
private
final
boolean
asroot
;
private
final
boolean
asroot
;
public
int
exitcode
=
-
1
;
public
int
exitcode
=
-
1
;
// private Process exec;
private
int
mProcId
;
private
FileDescriptor
mTermFd
;
/**
/**
* Creates a new script runner.
* Creates a new script runner.
*
*
* @param file temporary script file
* @param script script to run
* @param script script to run
* @param res
response
output
* @param res
ult result
output
* @param asroot if true, executes the script as root
* @param asroot if true, executes the script as root
*/
*/
public
ScriptRunner
(
File
file
,
String
script
,
StringBuilder
res
,
public
ScriptRunner
(
String
script
,
StringBuilder
result
,
boolean
asroot
)
{
boolean
asroot
)
{
this
.
file
=
file
;
this
.
scripts
=
script
;
this
.
script
=
script
;
this
.
result
=
result
;
this
.
res
=
res
;
this
.
asroot
=
asroot
;
this
.
asroot
=
asroot
;
}
}
private
int
createSubprocess
(
int
[]
processId
,
String
cmd
)
{
private
FileDescriptor
createSubprocess
(
int
[]
processId
,
String
cmd
)
{
ArrayList
<
String
>
argList
=
parse
(
cmd
);
ArrayList
<
String
>
argList
=
parse
(
cmd
);
String
arg0
=
argList
.
get
(
0
);
String
arg0
=
argList
.
get
(
0
);
String
[]
args
=
argList
.
toArray
(
new
String
[
1
]);
String
[]
args
=
argList
.
toArray
(
new
String
[
1
]);
mTermFd
=
Exec
.
createSubprocess
(
arg0
,
args
,
null
,
processId
);
return
Exec
.
createSubprocess
(
arg0
,
args
,
null
,
return
processId
[
0
];
scripts
+
"\nexit\n"
,
processId
);
}
/**
* Destroy this script runner
*/
@Override
public
synchronized
void
destroy
()
{
try
{
Exec
.
hangupProcessGroup
(
mProcId
);
Exec
.
close
(
mTermFd
);
}
catch
(
NoClassDefFoundError
ignore
)
{
// Nothing
}
}
}
private
ArrayList
<
String
>
parse
(
String
cmd
)
{
private
ArrayList
<
String
>
parse
(
String
cmd
)
{
...
@@ -114,61 +98,48 @@ public class Utils {
...
@@ -114,61 +98,48 @@ public class Utils {
@Override
@Override
public
void
run
()
{
public
void
run
()
{
FileDescriptor
pipe
=
null
;
int
pid
[]
=
new
int
[
1
];
pid
[
0
]
=
-
1
;
try
{
try
{
new
File
(
DEFOUT_FILE
).
createNewFile
();
file
.
createNewFile
();
final
String
abspath
=
file
.
getAbsolutePath
();
// TODO: Rewrite this line
// make sure we have execution permission on the script file
// Runtime.getRuntime().exec("chmod 755 " + abspath).waitFor();
// Write the script to be executed
final
OutputStreamWriter
out
=
new
OutputStreamWriter
(
new
FileOutputStream
(
file
));
out
.
write
(
"#!/system/bin/sh\n"
);
out
.
write
(
script
);
if
(!
script
.
endsWith
(
"\n"
))
out
.
write
(
"\n"
);
out
.
write
(
"exit\n"
);
out
.
flush
();
out
.
close
();
if
(
this
.
asroot
)
{
if
(
this
.
asroot
)
{
// Create the "su" request to run the script
// Create the "su" request to run the script
// exec = Runtime.getRuntime().exec(
pipe
=
createSubprocess
(
pid
,
root_shell
);
// root_shell + " -c " + abspath);
int
pid
[]
=
new
int
[
1
];
mProcId
=
createSubprocess
(
pid
,
root_shell
+
" -c "
+
abspath
);
}
else
{
}
else
{
// Create the "sh" request to run the script
// Create the "sh" request to run the script
// exec = Runtime.getRuntime().exec(getShell() + " " +
pipe
=
createSubprocess
(
pid
,
getShell
());
// abspath);
}
int
pid
[]
=
new
int
[
1
];
if
(
pipe
==
null
)
return
;
mProcId
=
createSubprocess
(
pid
,
getShell
()
+
" "
+
abspath
);
if
(
pid
[
0
]
!=
-
1
)
{
Exec
.
waitFor
(
pid
[
0
]);
}
}
final
InputStream
stdout
=
new
FileInputStream
(
DEFOUT_FILE
);
final
InputStream
stdout
=
new
FileInputStream
(
pipe
);
final
byte
buf
[]
=
new
byte
[
8192
];
final
byte
buf
[]
=
new
byte
[
8192
];
int
read
=
0
;
int
read
=
0
;
exitcode
=
Exec
.
waitFor
(
mProcId
);
// Read stdout
// Read stdout
while
(
stdout
.
available
()
>
0
)
{
while
(
stdout
.
available
()
>
0
)
{
read
=
stdout
.
read
(
buf
);
read
=
stdout
.
read
(
buf
);
if
(
res
!=
null
)
if
(
res
ult
!=
null
)
res
.
append
(
new
String
(
buf
,
0
,
read
));
res
ult
.
append
(
new
String
(
buf
,
0
,
read
));
}
}
}
catch
(
Exception
ex
)
{
}
catch
(
Exception
ex
)
{
if
(
res
!=
null
)
Log
.
e
(
TAG
,
"Cannot execute command"
,
ex
);
res
.
append
(
"\n"
+
ex
);
if
(
result
!=
null
)
result
.
append
(
"\n"
).
append
(
ex
);
}
finally
{
}
finally
{
destroy
();
if
(
pipe
!=
null
)
{
Exec
.
close
(
pipe
);
}
if
(
pid
[
0
]
!=
-
1
)
{
Exec
.
hangupProcessGroup
(
pid
[
0
]);
}
}
}
}
}
}
}
...
@@ -181,8 +152,6 @@ public class Utils {
...
@@ -181,8 +152,6 @@ public class Utils {
public
final
static
String
ALTERNATIVE_ROOT
=
"/system/xbin/su"
;
public
final
static
String
ALTERNATIVE_ROOT
=
"/system/xbin/su"
;
public
final
static
String
DEFAULT_IPTABLES
=
"/data/data/com.github.shadowsocks/iptables"
;
public
final
static
String
DEFAULT_IPTABLES
=
"/data/data/com.github.shadowsocks/iptables"
;
public
final
static
String
ALTERNATIVE_IPTABLES
=
"/system/bin/iptables"
;
public
final
static
String
ALTERNATIVE_IPTABLES
=
"/system/bin/iptables"
;
public
final
static
String
SCRIPT_FILE
=
"/data/data/com.github.shadowsocks/script"
;
public
final
static
String
DEFOUT_FILE
=
"/data/data/com.github.shadowsocks/defout"
;
public
final
static
int
TIME_OUT
=
-
99
;
public
final
static
int
TIME_OUT
=
-
99
;
private
static
boolean
initialized
=
false
;
private
static
boolean
initialized
=
false
;
...
@@ -281,7 +250,7 @@ public class Utils {
...
@@ -281,7 +250,7 @@ public class Utils {
public
static
boolean
getHasRedirectSupport
()
{
public
static
boolean
getHasRedirectSupport
()
{
if
(
hasRedirectSupport
==
-
1
)
if
(
hasRedirectSupport
==
-
1
)
initHasRedirectSupported
();
initHasRedirectSupported
();
return
hasRedirectSupport
==
1
?
true
:
false
;
return
hasRedirectSupport
==
1
;
}
}
public
static
String
getIptables
()
{
public
static
String
getIptables
()
{
...
@@ -338,7 +307,7 @@ public class Utils {
...
@@ -338,7 +307,7 @@ public class Utils {
public
static
boolean
isRoot
()
{
public
static
boolean
isRoot
()
{
if
(
isRoot
!=
-
1
)
if
(
isRoot
!=
-
1
)
return
isRoot
==
1
?
true
:
false
;
return
isRoot
==
1
;
// switch between binaries
// switch between binaries
if
(
new
File
(
DEFAULT_ROOT
).
exists
())
{
if
(
new
File
(
DEFAULT_ROOT
).
exists
())
{
...
@@ -352,7 +321,7 @@ public class Utils {
...
@@ -352,7 +321,7 @@ public class Utils {
String
lines
=
null
;
String
lines
=
null
;
StringBuilder
sb
=
new
StringBuilder
();
StringBuilder
sb
=
new
StringBuilder
();
String
command
=
"ls /\n
"
+
"
exit\n"
;
String
command
=
"ls /\n
exit\n"
;
int
exitcode
=
runScript
(
command
,
sb
,
10
*
1000
,
true
);
int
exitcode
=
runScript
(
command
,
sb
,
10
*
1000
,
true
);
...
@@ -366,7 +335,7 @@ public class Utils {
...
@@ -366,7 +335,7 @@ public class Utils {
isRoot
=
1
;
isRoot
=
1
;
}
}
return
isRoot
==
1
?
true
:
false
;
return
isRoot
==
1
;
}
}
public
static
boolean
runCommand
(
String
command
)
{
public
static
boolean
runCommand
(
String
command
)
{
...
@@ -392,8 +361,10 @@ public class Utils {
...
@@ -392,8 +361,10 @@ public class Utils {
public
static
boolean
runRootCommand
(
String
command
,
int
timeout
)
{
public
static
boolean
runRootCommand
(
String
command
,
int
timeout
)
{
if
(!
isRoot
())
if
(!
isRoot
())
{
Log
.
e
(
TAG
,
"Cannot get ROOT permission: "
+
root_shell
);
return
false
;
return
false
;
}
Log
.
d
(
TAG
,
command
);
Log
.
d
(
TAG
,
command
);
...
@@ -402,10 +373,9 @@ public class Utils {
...
@@ -402,10 +373,9 @@ public class Utils {
return
true
;
return
true
;
}
}
private
synchronized
static
int
runScript
(
String
script
,
StringBuilder
res
,
private
synchronized
static
int
runScript
(
String
script
,
StringBuilder
res
ult
,
long
timeout
,
boolean
asroot
)
{
long
timeout
,
boolean
asroot
)
{
final
File
file
=
new
File
(
SCRIPT_FILE
);
final
ScriptRunner
runner
=
new
ScriptRunner
(
script
,
result
,
asroot
);
final
ScriptRunner
runner
=
new
ScriptRunner
(
file
,
script
,
res
,
asroot
);
runner
.
start
();
runner
.
start
();
try
{
try
{
if
(
timeout
>
0
)
{
if
(
timeout
>
0
)
{
...
...
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