Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- require 'socket'
- require 'fileutils'
- $port = nil
- $hostname = nil
- # From config file.
- $updateInterval = nil
- $maxPayload = nil
- $pingTimeout = nil
- # Internal clock.
- $time = nil
- $clockDelay = 0.001
- # Keep track of ping times.
- $startTimes = Hash.new
- $startTimesSem = Mutex.new
- # All nodes' ports, with nodes => ports..
- $ports = Hash.new
- # All $hostname's sockets, with neighbors => sockets.
- $sockets = Hash.new
- $socketsSem = Mutex.new
- # All connected nodes, with sources => neighbors => costs.
- $neighbors = Hash.new { |h, k| h[k] = Hash.new }
- $neighborsSem = Mutex.new
- # All $hostname's shortest paths, with destinations => nextHops and distances.
- $paths = Hash.new
- $pathsSem = Mutex.new
- # All pending messages to/from $hostname, with UIDs => fragment arrays.
- $messages = Hash.new
- $messagesSem = Mutex.new
- # All filenames, with UIDs => filenames.
- $files = Hash.new
- $filesSem = Mutex.new
- class Path
- attr_accessor :nextHop, :distance
- def initialize(nextHop, distance)
- self.nextHop = nextHop
- self.distance = distance
- end
- end
- # --------------------- Part 0 --------------------- #
- def edgeb(cmd)
- srcIp = cmd[0]
- dstIp = cmd[1]
- dst = cmd[2]
- unless $sockets.key?(dst)
- socket = TCPSocket.new(dstIp, $ports[dst])
- link(dst, socket)
- $socketsSem.synchronize { socket.write("LINK #{$hostname}\0") }
- Thread.new { handleCommands(socket) }
- end
- end
- def dumptable(cmd)
- filename = cmd[0]
- File.open(filename, "w") do |f|
- $pathsSem.synchronize do
- $paths.keys.sort.each do |dst|
- f.puts("#{$hostname},#{dst},#{$paths[dst].nextHop},#{$paths[dst].distance}") unless dst == $hostname
- end
- end
- f.close
- end
- end
- def shutdown(cmd)
- $socketsSem.synchronize do
- $sockets.values.each do |sock|
- sock.write("EDGED #{$hostname}\0")
- sock.close
- end
- end
- STDOUT.flush
- exit(0)
- end
- # --------------------- Part 1 --------------------- #
- def edged(cmd)
- dst = cmd[0]
- exists = false
- $socketsSem.synchronize do
- if $sockets.key?(dst)
- exists = true
- $sockets[dst].close
- $sockets.delete(dst)
- end
- end
- if exists
- dEdge([$hostname, dst])
- # Find and delete all now-unreachable nodes.
- reachable = [$hostname]
- unreachable = []
- i = 0
- $neighborsSem.synchronize do
- while i < reachable.length
- reachable |= $neighbors[reachable[i]].keys
- i += 1
- end
- unreachable = $neighbors.keys - reachable
- end
- dNodes(unreachable)
- end
- end
- def edgeu(cmd)
- dst = cmd[0]
- cost = cmd[1]
- aEdges(["#{$hostname},#{dst},#{cost}"])
- end
- def status()
- STDOUT.write("Name: #{$hostname} Port: #{$port} Neighbors: #{$neighbors[$hostname].keys.sort.join(',')}\0")
- end
- # --------------------- Part 2 --------------------- #
- def sendmsg(cmd)
- dst = cmd[0]
- msg = cmd[1..-1].join(' ')
- uniq = 999
- seqId = 99999
- header = "M #{$hostname} #{dst} #{uniq} #{uniq} #{seqId} \0"
- messageSize = $maxPayload - header.bytesize
- numFrags = 0
- $startTimesSem.synchronize do
- loop do
- uniq = rand(999).to_s
- break unless $startTimes.key?(uniq)
- end
- $messagesSem.synchronize do
- $messages[uniq] = msg.scan(/.{1,#{messageSize}}/)
- numFrags = $messages[uniq].size
- end
- $startTimes[uniq] = $time
- end
- sendms([$hostname, dst, numFrags, uniq])
- Thread.new do
- sleep($pingTimeout)
- $startTimesSem.synchronize do
- unless $startTimes[uniq] == nil
- STDOUT.puts("SENDMSG ERROR: HOST UNREACHABLE")
- $messagesSem.synchronize { $messages.delete(uniq) }
- end
- $startTimes.delete(uniq)
- end
- end
- end
- def ping(cmd)
- dst = cmd[0]
- numPings = cmd[1].to_i
- delay = cmd[2].to_f
- Thread.new do
- numPings.times do |seqId|
- uniq = 0
- $startTimesSem.synchronize do
- loop do
- uniq = rand.to_s
- break unless $startTimes.key?(uniq)
- end
- $startTimes[uniq] = $time
- end
- sendp([$hostname, dst, seqId, uniq])
- Thread.new do
- sleep($pingTimeout)
- $startTimesSem.synchronize do
- STDOUT.puts("PING ERROR: HOST UNREACHABLE") unless $startTimes[uniq] == nil
- $startTimes.delete(uniq)
- end
- end
- sleep(delay) unless seqId == numPings - 1
- end
- end
- end
- def traceroute(cmd)
- dst = cmd[0]
- uniq = ""
- $startTimesSem.synchronize { $startTimes[uniq] = $time }
- ackt([$hostname, dst, $hostname, 0, uniq])
- end
- def ftp(cmd)
- dst = cmd[0]
- file = cmd[1]
- fpath = cmd[2]
- uniq = 999
- seqId = 9999999
- header = "M #{$hostname} #{dst} #{uniq} #{uniq} #{seqId} \0"
- messageSize = $maxPayload - header.bytesize
- $startTimesSem.synchronize do
- loop do
- uniq = rand(999).to_s
- break unless $startTimes.key?(uniq)
- end
- $startTimes[uniq] = nil
- end
- Thread.new do
- seqId = 0
- File.open(file, "r") do |f|
- $messagesSem.synchronize { $messages[uniq] = Array.new }
- while chunk = f.read(messageSize)
- $messagesSem.synchronize { $messages[uniq][seqId] = chunk }
- seqId += 1
- end
- f.close
- end
- numFrags = seqId
- $startTimesSem.synchronize { $startTimes[uniq] = $time }
- sendfs([$hostname, dst, numFrags, uniq, "#{fpath}/#{file}"])
- Thread.new do
- sleep($pingTimeout)
- $startTimesSem.synchronize do
- unless $startTimes[uniq] == nil
- STDOUT.puts("SENDMSG ERROR: HOST UNREACHABLE")
- $messagesSem.synchronize { $messages.delete(uniq) }
- end
- $startTimes.delete(uniq)
- end
- end
- end
- end
- # --------------------- Part 3 --------------------- #
- def circuit(cmd)
- STDOUT.puts("CIRCUIT: not implemented")
- end
- # ----------------- Custom Methods ----------------- #
- def link(node, socket)
- $socketsSem.synchronize { $sockets[node] = socket }
- edges = "AEDGES #{$hostname},#{node},1"
- $neighborsSem.synchronize { $neighbors.each { |src, dsts| dsts.each { |dst, cost| edges << " #{src},#{dst},#{cost}" } } }
- $socketsSem.synchronize { socket.write("#{edges}\0") }
- end
- def allSocketsPuts(line)
- $socketsSem.synchronize { $sockets.each_value { |sock| sock.write(line) } }
- end
- def aEdges(edges)
- edgesAdded = ""
- edges.each do |edge|
- arr = edge.split(',')
- src = arr[0]
- dst = arr[1]
- cost = arr[2].to_i
- $neighborsSem.synchronize do
- unless $neighbors.key?(src) && $neighbors[src].key?(dst) && $neighbors[src][dst] == cost
- $neighbors[src][dst] = $neighbors[dst][src] = cost
- edgesAdded << " #{edge}"
- end
- end
- end
- allSocketsPuts("AEDGES#{edgesAdded}\0") unless edgesAdded == ""
- end
- def dEdge(edge)
- src = edge[0]
- dst = edge[1]
- $neighborsSem.synchronize do
- if $neighbors.key?(src) && $neighbors[src].key?(dst)
- $neighbors[src].delete(dst)
- $neighbors.delete(src) if $neighbors[src].empty?
- $neighbors[dst].delete(src)
- $neighbors.delete(dst) if $neighbors[dst].empty?
- allSocketsPuts("DEDGE #{edge.join(' ')}\0")
- end
- end
- end
- def dNodes(nodes)
- nodesDeleted = ""
- nodes.each do |node|
- if $neighbors.key?(node)
- $neighborsSem.synchronize do
- $neighbors.each_value { |dsts| dsts.delete(node) }
- $neighbors.delete(node)
- end
- nodesDeleted << " #{node}"
- end
- end
- allSocketsPuts("DNODES#{nodesDeleted}\0") unless nodesDeleted == ""
- end
- def nextHopSocketPuts(dst, line)
- nextHop = ""
- $pathsSem.synchronize { nextHop = $paths[dst].nextHop if $paths.key?(dst) }
- $socketsSem.synchronize { $sockets[nextHop].write(line) if $sockets.key?(nextHop) }
- end
- def sendp(ping)
- src = ping[0]
- dst = ping[1]
- seqId = ping[2].to_i
- uniq = ping[3]
- dst == $hostname ? ackp(ping) : nextHopSocketPuts(dst, "SENDP #{ping.join(' ')}\0")
- end
- def ackp(ping)
- src = ping[0]
- dst = ping[1]
- seqId = ping[2].to_i
- uniq = ping[3]
- if src == $hostname
- $startTimesSem.synchronize do
- if $startTimes.key?(uniq)
- roundTripTime = $time - $startTimes[uniq]
- if roundTripTime < $pingTimeout
- $startTimes[uniq] = nil
- STDOUT.puts("#{seqId} #{dst} #{roundTripTime.round(3)}")
- end
- end
- end
- else
- nextHopSocketPuts(src, "ACKP #{ping.join(' ')}\0")
- end
- end
- def sendt(trace)
- src = trace[0]
- dst = trace[1]
- hops = trace[2].to_i
- uniq = trace[3]
- if hops == 0
- ackt([src, dst, $hostname, hops, uniq])
- else
- trace[2] = hops - 1
- nextHopSocketPuts(dst, "SENDT #{trace.join(' ')}\0")
- end
- end
- def ackt(trace)
- src = trace[0]
- dst = trace[1]
- hostId = trace[2]
- hopCount = trace[3].to_i
- uniq = trace[4]
- if src == $hostname
- sendTrace = false
- $startTimesSem.synchronize do
- if $startTimes.key?(uniq)
- roundTripTime = hostId == src ? 0.0 : ($time - $startTimes[uniq])
- if roundTripTime < $pingTimeout
- $startTimes[uniq] = nil
- STDOUT.puts("#{hopCount} #{hostId} #{(roundTripTime / 2).round(3)}")
- unless hostId == dst
- sendTrace = true
- loop do
- uniq = rand.to_s
- break unless $startTimes.key?(uniq)
- end
- $startTimes[uniq] = $time
- end
- end
- end
- end
- if sendTrace
- sendt([src, dst, hopCount + 1, uniq])
- Thread.new do
- sleep($pingTimeout)
- $startTimesSem.synchronize do
- STDOUT.puts("TIMEOUT ON #{hopCount + 1}") unless $startTimes[uniq] == nil
- $startTimes.delete(uniq)
- end
- end
- end
- else
- trace[3] = hopCount + 1
- nextHopSocketPuts(src, "ACKT #{trace.join(' ')}\0")
- end
- end
- def sendms(args)
- src = args[0]
- dst = args[1]
- numFrags = args[2].to_i
- srcUniq = args[3]
- if dst == $hostname
- uniq = 0
- $messagesSem.synchronize do
- loop do
- uniq = rand(999).to_s
- break unless $messages.key?(uniq)
- end
- $messages[uniq] = Array.new(numFrags)
- $startTimesSem.synchronize { $startTimes[uniq] = $time }
- end
- ackms([src, dst, srcUniq, uniq])
- STDERR.puts("out of acks")
- Thread.new do
- sleep($pingTimeout)
- STDERR.puts("after pingTimeout")
- $startTimesSem.synchronize do
- STDERR.puts("$startTimes.key?(uniq) = #{$startTimes.key?(uniq)}")
- STDERR.puts("($time - $startTimes[uniq]) = #{$time - $startTimes[uniq]}")
- unless $startTimes.key?(uniq) && ($time - $startTimes[uniq]) < $pingTimeout
- STDERR.puts("deleting")
- $startTimes.delete(uniq)
- $messagesSem.synchronize { $messages.delete(uniq) }
- end
- end
- end
- else
- nextHopSocketPuts(dst, "SENDMS #{args.join(' ')}\0")
- end
- end
- def ackms(args)
- src = args[0]
- dst = args[1]
- srcUniq = args[2]
- dstUniq = args[3]
- if src == $hostname
- $startTimesSem.synchronize { $startTimes.key?(srcUniq) && ($time - $startTimes[srcUniq]) < $pingTimeout ? $startTimes[srcUniq] = nil : return }
- numFrags = 0
- $messagesSem.synchronize { numFrags = $messages[srcUniq].size }
- numFrags.times do |seqId|
- uniq = 0
- $messagesSem.synchronize do
- $startTimesSem.synchronize do
- loop do
- uniq = rand(999).to_s
- break unless $startTimes.key?(uniq)
- end
- $startTimes[uniq] = $time
- end
- m([src, dst, uniq, dstUniq, seqId, $messages[srcUniq][seqId]])
- end
- Thread.new do
- sleep($pingTimeout)
- $startTimesSem.synchronize do
- $messagesSem.synchronize do
- if $startTimes[uniq] != nil && $messages.key?(srcUniq)
- STDOUT.puts("SNDMSG ERROR: HOST UNREACHABLE")
- $messages.delete(srcUniq)
- end
- $startTimes.delete(uniq)
- end
- end
- end
- end
- Thread.new do
- sleep($pingTimeout)
- $messagesSem.synchronize { $messages.delete(srcUniq) }
- end
- else
- nextHopSocketPuts(src, "ACKMS #{args.join(' ')}\0")
- end
- end
- def m(args)
- src = args[0]
- dst = args[1]
- srcUniq = args[2]
- uniq = args[3]
- seqId = args[4].to_i
- msg = args[5]
- if dst == $hostname
- stop = true
- $startTimesSem.synchronize do
- if $startTimes.key?(uniq)
- if $time - $startTimes[uniq] < $pingTimeout
- $startTimes[uniq] = $time
- stop = false
- end
- end
- end
- ackm([src, dst, srcUniq])
- $messagesSem.synchronize do
- unless stop
- $messages[uniq][seqId] = msg
- unless $messages[uniq].include?(nil)
- STDOUT.puts("SENDMSG: #{src} --> #{$messages[uniq].join('')}")
- stop = true
- end
- end
- if stop
- $messages.delete(uniq)
- $startTimesSem.synchronize { $startTimes.delete(uniq) }
- end
- end
- else
- nextHopSocketPuts(dst, "M #{args.join(' ')}\0")
- end
- end
- def ackm(args)
- src = args[0]
- dst = args[1]
- uniq = args[2]
- if src == $hostname
- $startTimesSem.synchronize { $startTimes[uniq] = nil if $startTimes.key?(uniq) && $time - $startTimes[uniq] < $pingTimeout }
- else
- nextHopSocketPuts(src, "ACKM #{args.join(' ')}\0")
- end
- end
- def sendfs(args)
- src = args[0]
- dst = args[1]
- numFrags = args[2].to_i
- srcUniq = args[3]
- file = args[4]
- if dst == $hostname
- uniq = 0
- $messagesSem.synchronize do
- loop do
- uniq = rand(999).to_s
- break unless $messages.key?(uniq)
- end
- $messages[uniq] = Array.new(numFrags)
- $filesSem.synchronize { $files[uniq] = file }
- $startTimesSem.synchronize { $startTimes[uniq] = $time }
- end
- ackfs([src, dst, srcUniq, uniq])
- STDERR.puts("out of acks")
- Thread.new do
- sleep($pingTimeout)
- STDERR.puts("after pingTimeout")
- $startTimesSem.synchronize do
- STDERR.puts("$startTimes.key?(uniq) = #{$startTimes.key?(uniq)}")
- STDERR.puts("($time - $startTimes[uniq]) = #{$time - $startTimes[uniq]}")
- unless $startTimes.key?(uniq) && ($time - $startTimes[uniq]) < $pingTimeout
- STDERR.puts("deleting")
- $startTimes.delete(uniq)
- $messagesSem.synchronize { $messages.delete(uniq) }
- $filesSem.synchronize { $files.delete(uniq) }
- end
- end
- end
- else
- nextHopSocketPuts(dst, "SENDFS #{args.join(' ')}\0")
- end
- end
- def ackfs(args)
- src = args[0]
- dst = args[1]
- srcUniq = args[2]
- dstUniq = args[3]
- if src == $hostname
- $startTimesSem.synchronize { $startTimes.key?(srcUniq) && ($time - $startTimes[srcUniq]) < $pingTimeout ? $startTimes[srcUniq] = nil : return }
- numFrags = 0
- $messagesSem.synchronize { numFrags = $messages[srcUniq].size }
- numFrags.times do |seqId|
- uniq = 0
- $messagesSem.synchronize do
- $startTimesSem.synchronize do
- loop do
- uniq = rand(999).to_s
- break unless $startTimes.key?(uniq)
- end
- $startTimes[uniq] = $time
- end
- f([src, dst, uniq, dstUniq, seqId, $messages[srcUniq][seqId]])
- end
- Thread.new do
- sleep($pingTimeout)
- $startTimesSem.synchronize do
- $messagesSem.synchronize do
- if $startTimes[uniq] != nil && $messages.key?(srcUniq)
- STDOUT.puts("FTP ERROR: HOST UNREACHABLE")
- $messages.delete(srcUniq)
- end
- $startTimes.delete(uniq)
- end
- end
- end
- end
- Thread.new do
- sleep($pingTimeout)
- $messagesSem.synchronize { $messages.delete(srcUniq) }
- end
- else
- nextHopSocketPuts(src, "ACKFS #{args.join(' ')}\0")
- end
- end
- def f(args)
- src = args[0]
- dst = args[1]
- srcUniq = args[2]
- uniq = args[3]
- seqId = args[4].to_i
- msg = args[5]
- STDERR.puts("in f")
- if dst == $hostname
- stop = true
- $startTimesSem.synchronize do
- if $startTimes.key?(uniq)
- if $time - $startTimes[uniq] < $pingTimeout
- $startTimes[uniq] = $time
- stop = false
- end
- end
- end
- ackf([src, dst, srcUniq])
- success = false
- $messagesSem.synchronize do
- unless stop
- $messages[uniq][seqId] = msg
- success = !$messages[uniq].include?(nil)
- end
- if stop
- $messages.delete(uniq)
- $filesSem.synchronize { $files.delete(uniq) }
- $startTimesSem.synchronize { $startTimes.delete(uniq) }
- end
- end
- if success
- file = ""
- $filesSem.synchronize { file = $files[uniq] }
- STDERR.puts(file)
- STDERR.puts(File.dirname(file))
- FileUtils.mkdir_p(File.dirname(file))
- Thread.new do
- File.open(file, "w") do |f|
- $messagesSem.synchronize { $messages[uniq].each { |chunk| f.print(chunk) } }
- f.close
- STDERR.puts("after close")
- end
- STDOUT.puts("FTP: #{src} --> #{file}")
- end
- end
- else
- nextHopSocketPuts(dst, "F #{args.join(' ')}\0")
- end
- end
- def ackf(args)
- src = args[0]
- dst = args[1]
- uniq = args[2]
- if src == $hostname
- $startTimesSem.synchronize { $startTimes[uniq] = nil if $startTimes.key?(uniq) && ($time - $startTimes[uniq]) < $pingTimeout }
- else
- nextHopSocketPuts(src, "ACKF #{args.join(' ')}\0")
- end
- end
- def handleCommands(socket)
- while line = socket.gets("\0").chomp("\0")
- #STDERR.puts(line)
- line.chomp!
- arr = line.split(' ')
- cmd = arr[0]
- args = arr[1..-1]
- case cmd
- when "LINK"; link(args[0], socket)
- when "AEDGES"; aEdges(args)
- when "DEDGE"; dEdge(args)
- when "DNODES"; dNodes(args)
- when "EDGED"; edged(args)
- when "SENDP"; sendp(args)
- when "ACKP"; ackp(args)
- when "SENDT"; sendt(args)
- when "ACKT"; ackt(args)
- when "SENDMS"; sendms(args)
- when "ACKMS"; ackms(args)
- when "M"; m(line.split(/ /, 7)[1..-1])
- when "ACKM"; ackm(args)
- when "SENDFS"; sendfs(args)
- when "ACKFS"; ackfs(args)
- when "F"; f(line.split(/ /, 7)[1..-1])
- when "ACKF"; ackf(args)
- end
- end
- end
- def dijkstra(src, dst)
- max = 2**(0.size * 8 - 2) - 1
- vertex = $neighbors.keys
- dists = Hash.new
- previous = Hash.new
- nodes = {}
- visited = []
- return if src == dst
- vertex.each do |k|
- if k == src
- dists[k] = 0
- nodes[k] = 0
- else
- dists[k] = max
- nodes[k] = max
- end
- previous[k] = nil
- end
- while nodes
- small = nodes.min_by { |k, v| v }
- name = small[0]
- if dists[name] == max || name == nil || nodes.empty?
- break
- end
- if visited.include?(name)
- nodes.delete(name)
- next
- end
- nodes.delete(name)
- $neighbors[name].each do |nei, c|
- tmp = dists[name] + c
- if tmp < dists[nei]
- dists[nei] = tmp
- previous[nei] = name
- nodes[nei] = tmp
- end
- unless previous[nei] == src
- prev = previous[nei]
- while previous[prev] != src && prev != nil
- prev = previous[prev]
- end
- else
- prev = nei
- end
- $paths[nei] = Path.new(prev, dists[nei])
- visited << name
- end
- if name == dst
- path = Array.new
- x = name
- while previous[name] != src && previous[name]
- name = previous[name]
- end
- if previous[x] == src
- name = x
- end
- $paths[x] = Path.new(name, dists[x])
- return
- end
- end
- end
- def server()
- server = TCPServer.new $port
- loop { Thread.new (server.accept) { |sock| handleCommands(sock) } }
- end
- def updateTime(delay)
- sleep(delay)
- $time += delay
- end
- def synchronizeTime()
- $time = Time.now.to_f
- updateTime($updateInterval - $time % $updateInterval)
- end
- def clock()
- synchronizeTime()
- Thread.new { pathsUpdater() }
- loop { updateTime($clockDelay) }
- end
- def pathsUpdater()
- loop do
- Thread.new do
- $pathsSem.synchronize do
- $paths.clear
- $neighborsSem.synchronize { $neighbors.each_key { |dst| dijkstra($hostname, dst) unless dst == $hostname } }
- end
- end
- sleep($updateInterval)
- end
- end
- # do main loop here....
- def main()
- while line = STDIN.gets
- #STDERR.puts("#{$hostname}: #{line}")
- line = line.strip
- arr = line.split(' ')
- cmd = arr[0]
- args = arr[1..-1]
- case cmd
- when "EDGEB"; edgeb(args)
- when "EDGED"; edged(args)
- when "EDGEU"; edgeu(args)
- when "DUMPTABLE"; dumptable(args)
- when "SHUTDOWN"; shutdown(args)
- when "STATUS"; status()
- when "SENDMSG"; sendmsg(args)
- when "PING"; ping(args)
- when "TRACEROUTE"; traceroute(args)
- when "FTP"; ftp(args)
- when "CIRCUIT"; circuit(args)
- else STDERR.puts "ERROR: INVALID COMMAND \"#{cmd}\""
- end
- end
- end
- def setup(hostname, port, nodes, config)
- $hostname = hostname
- $port = port
- # Set up ports, server, buffers.
- File.open(nodes, "r") do |f|
- f.each_line do |line|
- arr = line.split(',')
- n = arr[0]
- p = arr[1].to_i
- $ports[n] = p
- end
- f.close
- end
- File.open(config, "r") do |f|
- f.each_line do |line|
- arr = line.split('=')
- var = arr[0]
- val = arr[1].to_i
- case var
- when "updateInterval"; $updateInterval = val
- when "maxPayload"; $maxPayload = val
- when "pingTimeout"; $pingTimeout = val
- end
- end
- f.close
- end
- Thread.new { server() }
- Thread.new do
- synchronizeTime()
- Thread.new { clock() }
- pathsUpdater()
- end
- Thread.new do
- loop do
- $startTimesSem.synchronize { STDOUT.puts("#{$startTimes}") }
- sleep(0.01)
- end
- end
- main()
- end
- setup(ARGV[0], ARGV[1], ARGV[2], ARGV[3])
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement