Easy file sharing

By Dankirk on May 05, 2015

Made a mIRC script that allows you to share drag&dropped files with your friends on IRC.

Why?

To make things easy! No more uploading files to a 3rd party service, setting up/connecting to a DCC server or sending files one at a time to a single user.

How does it work?

When you drag&drop file(s) or folder(s) on a channel or query window the script creates a temporary webserver, and then pastes an URL to the shared files to the target window. Other users can then simply double click the URLs to access files with their favorite browser.

Setup

  1. Copy servefile.ini and servefile.txt into your mIRC folder.
  2. Edit servefile.ini to your liking.
  3. (Only if behind NAT w/o UPnP): Edit your router settings to route traffic from the specified port (default 81) to your LAN address.
  4. Run: /load -rs servefile.txt on any mIRC window.
  5. Go to mIRC Options->Mouse->Drop and replace:
*.wav:/sound $1 $2-
*.*:/dcc send $1 $2-

... with this:

*:/servestart $1 $2-

servefile.ini

[settings]
; Dedicated server address or leave empty for autoconfig.
ip=
port=81

; How many seconds will the webserver keep sharing a drag&dropped file.
servetime=3600

; How many seconds can an individual connection keep downloading a file.
max_download_time=1800

; How many simultaneous downloads can the webserver handle.
max_clients=50

servefile.txt

alias servestart {
  var %target = $1
  var %file = $noqt($2-)
  var %dir = 0

  if ($isdir(%file)) {
    %dir = 1
    var %files = $findfile(%file,*,0,noop)

    if (%files == 0) { return }
    else if (%files >= 5) {
      if (!$?!=" $+ $nopath(%file) has %files files in it. $crlf It's recommended to pack these files before sharing. $crlf $crlf Do you still want to share the files individually $+ ?") {
        return
      }
    }
  }

  ; Setup webserver and return if an error occurred
  if ($setup_webserver() == 0) { return }

  if (%dir) { noop $findfile(%file,*,0,sharefile %target $1-) }
  else { sharefile %target %file }
}

alias -l setup_webserver {

  ; Read settings from .ini file
  var %myserver = $readini(servefile.ini,n,settings,ip)
  if (!%myserver) { %myserver = $ip }

  var %port = $readini(servefile.ini,n,settings,port) 
  if (!%port) { %port = 81 }

  var %servetime = $readini(servefile.ini,n,settings,servetime) 
  if (!%servetime) { %servetime = 3600 }

  var %max_dowload_time = $readini(servefile.ini,n,settings,max_dowload_time) 
  if (!%max_dowload_time) { %max_dowload_time = 1800 }

  var %max_clients = $readini(servefile.ini,n,settings,max_clients) 
  if (!%max_clients) { %max_clients = 50 }

  ; Unshare any files left hanging (due to power loss, crash, etc)
  var %i = $var(%servefile.file.*,0)
  while (%i > 0) {
    if ($var(%servefile.file.*,%i).secs <= 0) { 
      unset [ $var(%servefile.file.*,%i) ]
    }
    dec %i
  }

  ; If port has been changed we need to restart the webserver
  ; However, leave already open connections intact
  if ($sock(servefile) && $sock(servefile).port != %port) { 
    sockclose servefile
    .timerservefile off
  }

  ; Start listening for clients. 
  ; If UPnP is enabled, portforward automatically (-p)
  if (!$sock(servefile)) {
    if (!$portfree(%port)) {
      echo %target ServeFile - Port %port is in use. Cannot create webserver.
      return 0
    }
    socklisten -p servefile %port
  }
  sockmark servefile %max_clients

  set -eu0 %servefile.webroot http:// $+ %myserver $+ : $+ %port $+ /
  set -eu0 %servefile.servetime %servetime

  .timerservefile 1 %servetime sockclose servefile
  .timerservefile.cleanup $ceil($calc(%servetime / 10)) 10 servefile.cleanup %max_dowload_time

  return 1
}

alias -l sharefile {

  var %target = $1
  var %file = $2-

  ; Generate a random string to prevent collision with files that have same name.
  ; If a particular file is already being shared, just reset the share timers
  var %id = 0
  var %randomname
  var %i = 1
  while (%i <= $var(%servefile.file.*,0)) {
    if ($var(%servefile.file.*,%i).value == %file) {
      %randomname = $gettok($var(%servefile.file.*,%i),-1,46)
      break
    }
    inc %i
  }

  if (!%randomname) { 
    while (!%randomname || %servefile.file. [ $+ [ %randomname ] ] ) { 
      %randomname = $randomstring()
    }
  }

  var %video_extensions = mp4 ogg webm
  var %is_video = $istok(%video_extensions, $gettok(%file,-1,46), 32)

  ; Share the file and post a link to it
  set -eu [ $+ [ %servefile.servetime ] ] %servefile.file. $+ %randomname %file
  msg %target %servefile.webroot $+ %randomname $+ / $+ $urlEncode($nopath(%file)) $+ $iif(%is_video, ?player=1)
}

alias -l urlDecode return $utfdecode($regsubex($replace($1, +, $chr(32)), /%([A-F\d]{2})/gi, $chr($base(\1, 16, 10))))
alias -l UrlEncode return $replace($regsubex($1-,/([^\d\w\-\_\.\!\'\(\) ])/g,$+(%,$base($asc(\t),10,16))),$chr(32),+)

; Just in case a browser doesn't play nicely, 
; we should disconnect them from our end after a timeout
alias -l servefile.cleanup {
  var %id = $sock(servefile.*,0)
  while (%id > 0) {
    if ($sock(servefile.*,%id).to > $1) {
      if ($gettok($sock(servefile.*,%id).mark,1,32) == content) {
        .fclose $gettok($sock(servefile.*,%id).mark,2,32)
      }
      sockclose $sock(servefile.*,%id)
    }
    dec %id
  }
}

alias -l randomstring {
  var %length  = 8
  var %ret
  while (%length > 0) {
    var %r = $rand(1,2)
    if (%r == 1) {
      %ret = %ret $+ $rand(a,z)
    }
    else {
      %ret = %ret $+ $rand(0,9)
    }
    dec %length
  }
  return %ret
}

; Returns path to an actual file on harddrive corresponding the requested URL
; If no shared file corresponds to given URL, returns null instead
alias -l getRealPath {
  var %path = %servefile.file. [ $+ [ $gettok($1-,1,47) ] ]
  if (%path && $nopath(%path) == $gettok($1-,2-,47)) {
    return %path
  }
  return $null
}

on 1:socklisten:servefile:{

  if ($sockerr) { return }

  ; Give an ID for connecting client,
  ; unless we already have too many clients
  var %id = 0
  while ($sock(servefile. $+ %id)) { 
    if (%id >= $sock($sockname).mark) { return }
    inc %id
  }

  sockaccept servefile. $+ %id
}

on 1:sockread:servefile.*:{
  if ($sockerr) { return }

  ; Ignore further requests if we are already serving a file to this client
  ; A separate connection is needed for each new request.
  if ($gettok($sock($sockname).mark,1,32) == content) {
    return
  }

  var %buffer
  var %file
  var %byterange = 0
  while ($sock($sockname)) {
    sockread -n %buffer
    if ($sockbr == 0) { break }

    ; We only need to handle GET requests from browser
    ; Anything else is irrelevant for our purposes
    if (!%file && $gettok(%buffer,1,32) == GET) {
      %file = $urlDecode($right($gettok(%buffer,2,32),-1))
    }
    if (%file && $gettok(%buffer,1,61) == Range: bytes) {
      %byterange = $gettok(%buffer,2,61)
    }
  }

  if (%file) { noop $serve($sockname, %byterange, %file) }
}

; We can only close sockets after all data has been written (after sockwrite event occurs and socket.sq == 0)
; Also we can't queue big files all at once, so send 8192 bytes at a time
on 1:sockwrite:servefile.*:{
  var %status = $gettok($sock($sockname).mark,1,32)
  var %stream = $gettok($sock($sockname).mark,2,32)
  var %range_max = $gettok($sock($sockname).mark,3,32)

  if ($sockerr) {
    if (%status == content) { .fclose %stream }
    sockclose $sockname
    return
  }

  if (%status == content) {

    ; Calculate how many bytes should we send. 
    ; Don't read past the requested byte range or queue more than 16384 bytes at a time
    var %toread = $iif($calc(%range_max - $fopen(%stream).pos) < 8192, $calc($v1 + 1), 8192)
    %toread = $iif($calc(16384 - $sock($sockname).sq - %toread) >= 0, %toread, $calc(%toread + $v1))

    ; echo -a Servefile: Reading %toread queue: $sock($sockname).sq

    if (%toread <= 0) { return }

    if ($fread(%stream, %toread, &buffer) > 0) {
      sockwrite $sockname &buffer
    }
    if ($fopen(%stream).eof || $fopen(%stream).err) {
      if ($sock($sockname).sq == 0) { sockclose $sockname }
      else { sockmark $sockname done }
      .fclose %stream
    }
  }
  else if (%status == done && $sock($sockname).sq == 0) {
    sockclose $sockname
  }
}

; If a connection gets interrupted, be sure to close any files we were sending
on 1:sockclose:servefile.*:{
  if ($gettok($sock($sockname).mark,1,32) == content) {
    .fclose $gettok($sock($sockname).mark,2,32)
  }
}

alias -l respond_notfound {

  var %html = <!DOCTYPE html><html><body><h1>Not Found</h1></body></html>

  sockmark $1 done

  sockwrite -n $1 HTTP/1.1 404 Not Found
  sockwrite -n $1 Content-Type: text/html
  sockwrite -n $1 Content-Length: $length(%html)
  sockwrite -n $1 Connection: close
  sockwrite -n $1
  sockwrite -n $1 %html
}

alias -l respond_embedded_player {

  var %html = <!DOCTYPE html> $&
    <html style="height:100%;"> $&
    <body style="margin:0px;height:100%;text-align:center;"> $&
    < $+ %element style="max-height:99%;max-width:99%" controls autoplay> $&
    <source src=" $+ $2- $+ ">Your browser doesn't support HTML5 $&
    </ $+ %element $+ > $& 
    <a target="_blank" href=" $+ $2- $+ ?dl=1">Download</a> $&
    </body> $&
    </html>

  sockmark $1 done

  sockwrite -n $1 HTTP/1.1 200 OK
  sockwrite -n $1 Content-Type: text/html
  sockwrite -n $1 Content-Length: $length(%html)
  sockwrite -n $1 Connection: close
  sockwrite -n $1
  sockwrite -n $1 %html
}

alias -l serve {
  var %path = $getRealPath($gettok($3-,1,63))

  if (!%path) {
    respond_notfound $1 $3-
    return
  }

  var %params = $gettok($3-,2,63)
  var %embed_player = $istok(%params, player=1, 38)

  if (%embed_player) {
    respond_embedded_player $1 $urlEncode($nopath(%path))
    return
  }

  var %id = 0
  while ($fopen(servefile. $+ %id)) { inc %id }
  var %stream = servefile. $+ %id
  .fopen %stream $qt(%path)

  if ($fopen(%stream).err) {
    echo -ta Servefile: Unable to access shared file: %path
    .fclose %stream    
    respond_notfound $1 %path
    return
  }

  ; If a specific range is requested, seek to beginning of that position
  var %filesize = $file(%path).size
  var %byterange_min = $iif($calc($gettok($2,1,45)) >= 0, $v1, 0)
  var %byterange_max = $iif($numtok($2,45) == 2 && $calc($gettok($2,2,45)) >= %byterange_min, $v1, $calc(%filesize - 1))
  var %partial = $iif($chr(45) isin $2, 1, 0)

  .fseek %stream %byterange_min

  if ($fopen(%stream).err || $fopen(%stream).eof) {
    .fclose %stream    
    respond_notfound $1 %path
    return
  }

  ; echo -a $timestamp Servefile: $sock($1).ip requested a shared file: $nopath(%path)

  ; If sharing a file with web content, force plain text mode.
  ; Otherwise let browser decide
  var %web_extensions = acgi htm html htmls htt htx shtml php asp aspx css cer csr js jsp rss xhtml
  var %force_plaintext = $istok(%web_extensions, $gettok(%path,-1,46), 32)

  sockmark $1 content %stream %byterange_max

  sockwrite -n $1 HTTP/1.1 $iif(%partial, 206 Partial Content, 200 OK)
  sockwrite -n $1 Content-Disposition: filename=" $+ $nopath(%path) $+ "
  sockwrite -n $1 Content-Length: $calc(%byterange_max - %byterange_min + 1)
  if (%partial) {
    ; echo -a $timestamp Servefile: $sock($1).ip resuming $nopath(%path) from %byterange_min to %byterange_max ( %filesize )
    sockwrite -n $1 Accept-Ranges: bytes
    sockwrite -n $1 Content-Range: bytes %byterange_min $+ - $+ %byterange_max $+ / $+ %filesize
  }
  if (%force_plaintext) {
    sockwrite -n $1 Content-Type: text/plain
  }
  sockwrite -n $1 Connection: close
  sockwrite -n $1
}

Comments

Sign in to comment.
Dankirk   -  Mar 08, 2016

Updates for the script.

Changelog:

  • [ui] Prettier URLs
  • [ui] Existing webserver settings updated when drag&dropping more files
  • [ui] Increased default download time limit to 30 minutes.
  • [bug] Fix for sharing files with web content (.html .php, etc). They are now shown in plain text.
  • [bug] Error handling for inaccessible files
  • [bug] Properly remove previous shares after a power loss, crash, etc
  • [bug] Bug fix for id handling when sharing an already shared file.
  • [performance] Webserver settings read/updated only once per drag&drop event. (previously once per file shared)
Dankirk  -  Mar 10, 2016

More updates:

  • [bug] URLs support UTF8 properly
  • [performance] Revised socket reading/writing. It's a bit faster now.
Dankirk  -  Mar 23, 2016

More updates:

  • [feature] Support for download ranges. This enables seeking for video/audio content and pausing/resuming for other downloads.
Dankirk  -  Mar 25, 2016

More updates:

  • [feature] mp4, ogg and webm files are now shared with player=1 parameter in the url, which enables content streaming via <video> element.
  • [bug] Fixed some incorrect headers
  • [performance] Reworked random ID generation and matching. It's a bit faster now.
Sign in to comment

Are you sure you want to unfollow this person?
Are you sure you want to delete this?
Click "Unsubscribe" to stop receiving notices pertaining to this post.
Click "Subscribe" to resume notices pertaining to this post.