Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
G
Geth-Modification
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
张蕾
Geth-Modification
Commits
1c140f73
Commit
1c140f73
authored
Jan 30, 2017
by
Péter Szilágyi
Committed by
GitHub
Jan 30, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #3615 from nolash/bzzpathfix_real5
cmd/swarm, swarm/api: bzzr improve + networkid prio
parents
f3c368ca
e5a93bf9
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
187 additions
and
15 deletions
+187
-15
main.go
cmd/swarm/main.go
+3
-2
api.go
swarm/api/api.go
+10
-3
config.go
swarm/api/config.go
+14
-0
server.go
swarm/api/http/server.go
+25
-7
server_test.go
swarm/api/http/server_test.go
+133
-0
manifest.go
swarm/api/manifest.go
+2
-3
No files found.
cmd/swarm/main.go
View file @
1c140f73
...
@@ -39,7 +39,6 @@ import (
...
@@ -39,7 +39,6 @@ import (
"github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/swarm"
"github.com/ethereum/go-ethereum/swarm"
bzzapi
"github.com/ethereum/go-ethereum/swarm/api"
bzzapi
"github.com/ethereum/go-ethereum/swarm/api"
"github.com/ethereum/go-ethereum/swarm/network"
"gopkg.in/urfave/cli.v1"
"gopkg.in/urfave/cli.v1"
)
)
...
@@ -76,7 +75,6 @@ var (
...
@@ -76,7 +75,6 @@ var (
SwarmNetworkIdFlag
=
cli
.
IntFlag
{
SwarmNetworkIdFlag
=
cli
.
IntFlag
{
Name
:
"bzznetworkid"
,
Name
:
"bzznetworkid"
,
Usage
:
"Network identifier (integer, default 3=swarm testnet)"
,
Usage
:
"Network identifier (integer, default 3=swarm testnet)"
,
Value
:
network
.
NetworkId
,
}
}
SwarmConfigPathFlag
=
cli
.
StringFlag
{
SwarmConfigPathFlag
=
cli
.
StringFlag
{
Name
:
"bzzconfig"
,
Name
:
"bzzconfig"
,
...
@@ -242,6 +240,7 @@ func bzzd(ctx *cli.Context) error {
...
@@ -242,6 +240,7 @@ func bzzd(ctx *cli.Context) error {
}
}
func
registerBzzService
(
ctx
*
cli
.
Context
,
stack
*
node
.
Node
)
{
func
registerBzzService
(
ctx
*
cli
.
Context
,
stack
*
node
.
Node
)
{
prvkey
:=
getAccount
(
ctx
,
stack
)
prvkey
:=
getAccount
(
ctx
,
stack
)
chbookaddr
:=
common
.
HexToAddress
(
ctx
.
GlobalString
(
ChequebookAddrFlag
.
Name
))
chbookaddr
:=
common
.
HexToAddress
(
ctx
.
GlobalString
(
ChequebookAddrFlag
.
Name
))
...
@@ -249,6 +248,7 @@ func registerBzzService(ctx *cli.Context, stack *node.Node) {
...
@@ -249,6 +248,7 @@ func registerBzzService(ctx *cli.Context, stack *node.Node) {
if
bzzdir
==
""
{
if
bzzdir
==
""
{
bzzdir
=
stack
.
InstanceDir
()
bzzdir
=
stack
.
InstanceDir
()
}
}
bzzconfig
,
err
:=
bzzapi
.
NewConfig
(
bzzdir
,
chbookaddr
,
prvkey
,
ctx
.
GlobalUint64
(
SwarmNetworkIdFlag
.
Name
))
bzzconfig
,
err
:=
bzzapi
.
NewConfig
(
bzzdir
,
chbookaddr
,
prvkey
,
ctx
.
GlobalUint64
(
SwarmNetworkIdFlag
.
Name
))
if
err
!=
nil
{
if
err
!=
nil
{
utils
.
Fatalf
(
"unable to configure swarm: %v"
,
err
)
utils
.
Fatalf
(
"unable to configure swarm: %v"
,
err
)
...
@@ -280,6 +280,7 @@ func registerBzzService(ctx *cli.Context, stack *node.Node) {
...
@@ -280,6 +280,7 @@ func registerBzzService(ctx *cli.Context, stack *node.Node) {
func
getAccount
(
ctx
*
cli
.
Context
,
stack
*
node
.
Node
)
*
ecdsa
.
PrivateKey
{
func
getAccount
(
ctx
*
cli
.
Context
,
stack
*
node
.
Node
)
*
ecdsa
.
PrivateKey
{
keyid
:=
ctx
.
GlobalString
(
SwarmAccountFlag
.
Name
)
keyid
:=
ctx
.
GlobalString
(
SwarmAccountFlag
.
Name
)
if
keyid
==
""
{
if
keyid
==
""
{
utils
.
Fatalf
(
"Option %q is required"
,
SwarmAccountFlag
.
Name
)
utils
.
Fatalf
(
"Option %q is required"
,
SwarmAccountFlag
.
Name
)
}
}
...
...
swarm/api/api.go
View file @
1c140f73
...
@@ -19,6 +19,7 @@ package api
...
@@ -19,6 +19,7 @@ package api
import
(
import
(
"fmt"
"fmt"
"io"
"io"
"net/http"
"regexp"
"regexp"
"strings"
"strings"
"sync"
"sync"
...
@@ -71,6 +72,7 @@ type ErrResolve error
...
@@ -71,6 +72,7 @@ type ErrResolve error
// DNS Resolver
// DNS Resolver
func
(
self
*
Api
)
Resolve
(
hostPort
string
,
nameresolver
bool
)
(
storage
.
Key
,
error
)
{
func
(
self
*
Api
)
Resolve
(
hostPort
string
,
nameresolver
bool
)
(
storage
.
Key
,
error
)
{
glog
.
V
(
logger
.
Detail
)
.
Infof
(
"Resolving : %v"
,
hostPort
)
if
hashMatcher
.
MatchString
(
hostPort
)
||
self
.
dns
==
nil
{
if
hashMatcher
.
MatchString
(
hostPort
)
||
self
.
dns
==
nil
{
glog
.
V
(
logger
.
Detail
)
.
Infof
(
"host is a contentHash: '%v'"
,
hostPort
)
glog
.
V
(
logger
.
Detail
)
.
Infof
(
"host is a contentHash: '%v'"
,
hostPort
)
return
storage
.
Key
(
common
.
Hex2Bytes
(
hostPort
)),
nil
return
storage
.
Key
(
common
.
Hex2Bytes
(
hostPort
)),
nil
...
@@ -86,8 +88,10 @@ func (self *Api) Resolve(hostPort string, nameresolver bool) (storage.Key, error
...
@@ -86,8 +88,10 @@ func (self *Api) Resolve(hostPort string, nameresolver bool) (storage.Key, error
glog
.
V
(
logger
.
Detail
)
.
Infof
(
"host lookup: %v -> %v"
,
err
)
glog
.
V
(
logger
.
Detail
)
.
Infof
(
"host lookup: %v -> %v"
,
err
)
return
contentHash
[
:
],
err
return
contentHash
[
:
],
err
}
}
func
Parse
(
uri
string
)
(
hostPort
,
path
string
)
{
func
parse
(
uri
string
)
(
hostPort
,
path
string
)
{
if
uri
==
""
{
return
}
parts
:=
slashes
.
Split
(
uri
,
3
)
parts
:=
slashes
.
Split
(
uri
,
3
)
var
i
int
var
i
int
if
len
(
parts
)
==
0
{
if
len
(
parts
)
==
0
{
...
@@ -111,7 +115,7 @@ func parse(uri string) (hostPort, path string) {
...
@@ -111,7 +115,7 @@ func parse(uri string) (hostPort, path string) {
}
}
func
(
self
*
Api
)
parseAndResolve
(
uri
string
,
nameresolver
bool
)
(
key
storage
.
Key
,
hostPort
,
path
string
,
err
error
)
{
func
(
self
*
Api
)
parseAndResolve
(
uri
string
,
nameresolver
bool
)
(
key
storage
.
Key
,
hostPort
,
path
string
,
err
error
)
{
hostPort
,
path
=
p
arse
(
uri
)
hostPort
,
path
=
P
arse
(
uri
)
//resolving host and port
//resolving host and port
contentHash
,
err
:=
self
.
Resolve
(
hostPort
,
nameresolver
)
contentHash
,
err
:=
self
.
Resolve
(
hostPort
,
nameresolver
)
glog
.
V
(
logger
.
Debug
)
.
Infof
(
"Resolved '%s' to contentHash: '%s', path: '%s'"
,
uri
,
contentHash
,
path
)
glog
.
V
(
logger
.
Debug
)
.
Infof
(
"Resolved '%s' to contentHash: '%s', path: '%s'"
,
uri
,
contentHash
,
path
)
...
@@ -153,7 +157,9 @@ func (self *Api) Get(uri string, nameresolver bool) (reader storage.LazySectionR
...
@@ -153,7 +157,9 @@ func (self *Api) Get(uri string, nameresolver bool) (reader storage.LazySectionR
}
}
glog
.
V
(
logger
.
Detail
)
.
Infof
(
"getEntry(%s)"
,
path
)
glog
.
V
(
logger
.
Detail
)
.
Infof
(
"getEntry(%s)"
,
path
)
entry
,
_
:=
trie
.
getEntry
(
path
)
entry
,
_
:=
trie
.
getEntry
(
path
)
if
entry
!=
nil
{
if
entry
!=
nil
{
key
=
common
.
Hex2Bytes
(
entry
.
Hash
)
key
=
common
.
Hex2Bytes
(
entry
.
Hash
)
status
=
entry
.
Status
status
=
entry
.
Status
...
@@ -161,6 +167,7 @@ func (self *Api) Get(uri string, nameresolver bool) (reader storage.LazySectionR
...
@@ -161,6 +167,7 @@ func (self *Api) Get(uri string, nameresolver bool) (reader storage.LazySectionR
glog
.
V
(
logger
.
Detail
)
.
Infof
(
"content lookup key: '%v' (%v)"
,
key
,
mimeType
)
glog
.
V
(
logger
.
Detail
)
.
Infof
(
"content lookup key: '%v' (%v)"
,
key
,
mimeType
)
reader
=
self
.
dpa
.
Retrieve
(
key
)
reader
=
self
.
dpa
.
Retrieve
(
key
)
}
else
{
}
else
{
status
=
http
.
StatusNotFound
err
=
fmt
.
Errorf
(
"manifest entry for '%s' not found"
,
path
)
err
=
fmt
.
Errorf
(
"manifest entry for '%s' not found"
,
path
)
glog
.
V
(
logger
.
Warn
)
.
Infof
(
"%v"
,
err
)
glog
.
V
(
logger
.
Warn
)
.
Infof
(
"%v"
,
err
)
}
}
...
...
swarm/api/config.go
View file @
1c140f73
...
@@ -85,10 +85,17 @@ func NewConfig(path string, contract common.Address, prvKey *ecdsa.PrivateKey, n
...
@@ -85,10 +85,17 @@ func NewConfig(path string, contract common.Address, prvKey *ecdsa.PrivateKey, n
NetworkId
:
networkId
,
NetworkId
:
networkId
,
}
}
data
,
err
=
ioutil
.
ReadFile
(
confpath
)
data
,
err
=
ioutil
.
ReadFile
(
confpath
)
// if not set in function param, then set default for swarm network, will be overwritten by config file if present
if
networkId
==
0
{
self
.
NetworkId
=
network
.
NetworkId
}
if
err
!=
nil
{
if
err
!=
nil
{
if
!
os
.
IsNotExist
(
err
)
{
if
!
os
.
IsNotExist
(
err
)
{
return
return
}
}
// file does not exist
// file does not exist
// write out config file
// write out config file
err
=
self
.
Save
()
err
=
self
.
Save
()
...
@@ -97,6 +104,7 @@ func NewConfig(path string, contract common.Address, prvKey *ecdsa.PrivateKey, n
...
@@ -97,6 +104,7 @@ func NewConfig(path string, contract common.Address, prvKey *ecdsa.PrivateKey, n
}
}
return
return
}
}
// file exists, deserialise
// file exists, deserialise
err
=
json
.
Unmarshal
(
data
,
self
)
err
=
json
.
Unmarshal
(
data
,
self
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -109,6 +117,12 @@ func NewConfig(path string, contract common.Address, prvKey *ecdsa.PrivateKey, n
...
@@ -109,6 +117,12 @@ func NewConfig(path string, contract common.Address, prvKey *ecdsa.PrivateKey, n
if
keyhex
!=
self
.
BzzKey
{
if
keyhex
!=
self
.
BzzKey
{
return
nil
,
fmt
.
Errorf
(
"bzz key does not match the one in the config file %v != %v"
,
keyhex
,
self
.
BzzKey
)
return
nil
,
fmt
.
Errorf
(
"bzz key does not match the one in the config file %v != %v"
,
keyhex
,
self
.
BzzKey
)
}
}
// if set in function param, replace id set from config file
if
networkId
!=
0
{
self
.
NetworkId
=
networkId
}
self
.
Swap
.
SetKey
(
prvKey
)
self
.
Swap
.
SetKey
(
prvKey
)
if
(
self
.
EnsRoot
==
common
.
Address
{})
{
if
(
self
.
EnsRoot
==
common
.
Address
{})
{
...
...
swarm/api/http/server.go
View file @
1c140f73
...
@@ -32,6 +32,7 @@ import (
...
@@ -32,6 +32,7 @@ import (
"github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog"
"github.com/ethereum/go-ethereum/logger/glog"
"github.com/ethereum/go-ethereum/swarm/api"
"github.com/ethereum/go-ethereum/swarm/api"
"github.com/ethereum/go-ethereum/swarm/storage"
"github.com/rs/cors"
"github.com/rs/cors"
)
)
...
@@ -194,17 +195,34 @@ func handler(w http.ResponseWriter, r *http.Request, a *api.Api) {
...
@@ -194,17 +195,34 @@ func handler(w http.ResponseWriter, r *http.Request, a *api.Api) {
}
}
case
r
.
Method
==
"GET"
||
r
.
Method
==
"HEAD"
:
case
r
.
Method
==
"GET"
||
r
.
Method
==
"HEAD"
:
path
=
trailingSlashes
.
ReplaceAllString
(
path
,
""
)
path
=
trailingSlashes
.
ReplaceAllString
(
path
,
""
)
if
path
==
""
{
http
.
Error
(
w
,
"Empty path not allowed"
,
http
.
StatusBadRequest
)
return
}
if
raw
{
if
raw
{
// resolving host
var
reader
storage
.
LazySectionReader
key
,
err
:=
a
.
Resolve
(
path
,
nameresolver
)
parsedurl
,
_
:=
api
.
Parse
(
path
)
if
err
!=
nil
{
glog
.
V
(
logger
.
Error
)
.
Infof
(
"%v"
,
err
)
if
parsedurl
==
path
{
http
.
Error
(
w
,
err
.
Error
(),
http
.
StatusBadRequest
)
key
,
err
:=
a
.
Resolve
(
parsedurl
,
nameresolver
)
return
if
err
!=
nil
{
glog
.
V
(
logger
.
Error
)
.
Infof
(
"%v"
,
err
)
http
.
Error
(
w
,
err
.
Error
(),
http
.
StatusBadRequest
)
return
}
reader
=
a
.
Retrieve
(
key
)
}
else
{
var
status
int
readertmp
,
_
,
status
,
err
:=
a
.
Get
(
path
,
nameresolver
)
if
err
!=
nil
{
http
.
Error
(
w
,
err
.
Error
(),
status
)
return
}
reader
=
readertmp
}
}
// retrieving content
// retrieving content
reader
:=
a
.
Retrieve
(
key
)
quitC
:=
make
(
chan
bool
)
quitC
:=
make
(
chan
bool
)
size
,
err
:=
reader
.
Size
(
quitC
)
size
,
err
:=
reader
.
Size
(
quitC
)
if
err
!=
nil
{
if
err
!=
nil
{
...
...
swarm/api/http/server_test.go
0 → 100644
View file @
1c140f73
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library 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. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package
http
import
(
"bytes"
"io/ioutil"
"net/http"
"sync"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/swarm/api"
"github.com/ethereum/go-ethereum/swarm/storage"
)
func
TestBzzrGetPath
(
t
*
testing
.
T
)
{
var
err
error
maxproxyattempts
:=
3
testmanifest
:=
[]
string
{
`{"entries":[{"path":"a/","hash":"674af7073604ebfc0282a4ab21e5ef1a3c22913866879ebc0816f8a89896b2ed","contentType":"application/bzz-manifest+json","status":0}]}`
,
`{"entries":[{"path":"a","hash":"011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce","contentType":"","status":0},{"path":"b/","hash":"0a87b1c3e4bf013686cdf107ec58590f2004610ee58cc2240f26939f691215f5","contentType":"application/bzz-manifest+json","status":0}]}`
,
`{"entries":[{"path":"b","hash":"011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce","contentType":"","status":0},{"path":"c","hash":"011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce","contentType":"","status":0}]}`
,
}
testrequests
:=
make
(
map
[
string
]
int
)
testrequests
[
"/"
]
=
0
testrequests
[
"/a"
]
=
1
testrequests
[
"/a/b"
]
=
2
testrequests
[
"/x"
]
=
0
testrequests
[
""
]
=
0
expectedfailrequests
:=
[]
string
{
""
,
"/x"
}
reader
:=
[
3
]
*
bytes
.
Reader
{}
key
:=
[
3
]
storage
.
Key
{}
dir
,
_
:=
ioutil
.
TempDir
(
""
,
"bzz-storage-test"
)
storeparams
:=
&
storage
.
StoreParams
{
ChunkDbPath
:
dir
,
DbCapacity
:
5000000
,
CacheCapacity
:
5000
,
Radius
:
0
,
}
localStore
,
err
:=
storage
.
NewLocalStore
(
storage
.
MakeHashFunc
(
"SHA3"
),
storeparams
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
chunker
:=
storage
.
NewTreeChunker
(
storage
.
NewChunkerParams
())
dpa
:=
&
storage
.
DPA
{
Chunker
:
chunker
,
ChunkStore
:
localStore
,
}
dpa
.
Start
()
defer
dpa
.
Stop
()
wg
:=
&
sync
.
WaitGroup
{}
for
i
,
mf
:=
range
testmanifest
{
reader
[
i
]
=
bytes
.
NewReader
([]
byte
(
mf
))
key
[
i
],
err
=
dpa
.
Store
(
reader
[
i
],
int64
(
len
(
mf
)),
wg
,
nil
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
wg
.
Wait
()
}
a
:=
api
.
NewApi
(
dpa
,
nil
)
/// \todo iterate port numbers up if fail
StartHttpServer
(
a
,
&
Server
{
Addr
:
"127.0.0.1:8504"
,
CorsString
:
""
})
// how to wait for ListenAndServe to have initialized? This is pretty cruuuude
// if we fix it we don't need maxproxyattempts anymore either
time
.
Sleep
(
1000
*
time
.
Millisecond
)
for
i
:=
0
;
i
<=
maxproxyattempts
;
i
++
{
_
,
err
:=
http
.
Get
(
"http://127.0.0.1:8504/bzzr:/"
+
common
.
ToHex
(
key
[
0
])[
2
:
]
+
"/a"
)
if
i
==
maxproxyattempts
{
t
.
Fatalf
(
"Failed to connect to proxy after %v attempts: %v"
,
i
,
err
)
}
else
if
err
!=
nil
{
time
.
Sleep
(
100
*
time
.
Millisecond
)
continue
}
break
}
for
k
,
v
:=
range
testrequests
{
var
resp
*
http
.
Response
var
respbody
[]
byte
url
:=
"http://127.0.0.1:8504/bzzr:/"
if
k
[
:
]
!=
""
{
url
+=
common
.
ToHex
(
key
[
0
])[
2
:
]
+
"/"
+
k
[
1
:
]
+
"?content_type=text/plain"
}
resp
,
err
=
http
.
Get
(
url
)
defer
resp
.
Body
.
Close
()
respbody
,
err
=
ioutil
.
ReadAll
(
resp
.
Body
)
if
string
(
respbody
)
!=
testmanifest
[
v
]
{
isexpectedfailrequest
:=
false
for
_
,
r
:=
range
expectedfailrequests
{
if
k
[
:
]
==
r
{
isexpectedfailrequest
=
true
}
}
if
isexpectedfailrequest
==
false
{
t
.
Fatalf
(
"Response body does not match, expected: %v, got %v"
,
testmanifest
[
v
],
string
(
respbody
))
}
}
}
}
swarm/api/manifest.go
View file @
1c140f73
...
@@ -302,7 +302,8 @@ func (self *manifestTrie) findPrefixOf(path string, quitC chan bool) (entry *man
...
@@ -302,7 +302,8 @@ func (self *manifestTrie) findPrefixOf(path string, quitC chan bool) (entry *man
if
(
len
(
path
)
>=
epl
)
&&
(
path
[
:
epl
]
==
entry
.
Path
)
{
if
(
len
(
path
)
>=
epl
)
&&
(
path
[
:
epl
]
==
entry
.
Path
)
{
glog
.
V
(
logger
.
Detail
)
.
Infof
(
"entry.ContentType = %v"
,
entry
.
ContentType
)
glog
.
V
(
logger
.
Detail
)
.
Infof
(
"entry.ContentType = %v"
,
entry
.
ContentType
)
if
entry
.
ContentType
==
manifestType
{
if
entry
.
ContentType
==
manifestType
{
if
self
.
loadSubTrie
(
entry
,
quitC
)
!=
nil
{
err
:=
self
.
loadSubTrie
(
entry
,
quitC
)
if
err
!=
nil
{
return
nil
,
0
return
nil
,
0
}
}
entry
,
pos
=
entry
.
subtrie
.
findPrefixOf
(
path
[
epl
:
],
quitC
)
entry
,
pos
=
entry
.
subtrie
.
findPrefixOf
(
path
[
epl
:
],
quitC
)
...
@@ -312,8 +313,6 @@ func (self *manifestTrie) findPrefixOf(path string, quitC chan bool) (entry *man
...
@@ -312,8 +313,6 @@ func (self *manifestTrie) findPrefixOf(path string, quitC chan bool) (entry *man
}
else
{
}
else
{
pos
=
epl
pos
=
epl
}
}
}
else
{
entry
=
nil
}
}
return
return
}
}
...
...
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