summaryrefslogtreecommitdiffstats
path: root/scripts/daap-get-library.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/daap-get-library.nse')
-rw-r--r--scripts/daap-get-library.nse332
1 files changed, 332 insertions, 0 deletions
diff --git a/scripts/daap-get-library.nse b/scripts/daap-get-library.nse
new file mode 100644
index 0000000..a39fca4
--- /dev/null
+++ b/scripts/daap-get-library.nse
@@ -0,0 +1,332 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Retrieves a list of music from a DAAP server. The list includes artist
+names and album and song titles.
+
+Output will be capped to 100 items if not otherwise specified in the
+<code>daap_item_limit</code> script argument. A
+<code>daap_item_limit</code> below zero outputs the complete contents of
+the DAAP library.
+
+Based on documentation found here:
+http://www.tapjam.net/daap/.
+]]
+
+---
+-- @args daap_item_limit Changes the output limit from 100 songs. If set to a negative value, no limit is enforced.
+--
+-- @output
+-- | daap-get-library:
+-- | BUBBA|TWO
+-- | Fever Ray
+-- | Fever Ray (Deluxe Edition)
+-- | Concrete Walls
+-- | I'm Not Done
+-- | Here Before
+-- | Now's The Only Time I Know
+-- | Stranger Than Kindness
+-- | Dry And Dusty
+-- | Keep The Streets Empty For Me
+-- | Triangle Walks
+-- | If I Had A Heart
+-- | Seven
+-- | When I Grow Up
+-- |_ Coconut
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+-- Version 0.2
+-- Created 01/14/2010 - v0.1 - created by Patrik Karlsson
+-- Revised 01/23/2010 - v0.2 - changed to port_or_service, added link to documentation, limited output to 100 songs or to daap_item_limit script argument.
+
+portrule = shortport.port_or_service(3689, "daap")
+
+--- Gets the name of the library from the server
+--
+-- @param host table containing an ip field.
+-- @param port table containing number and protocol fields.
+-- @return string containing the name of the library
+function getLibraryName( host, port )
+ local libname, pos
+ local url = "daap://" .. host.ip .. "/server-info"
+ local response = http.get( host, port, url, nil, nil, nil)
+
+ if response == nil or response.body == nil or response.body=="" then
+ return
+ end
+
+ pos = string.find(response.body, "minm")
+
+ if pos > 0 then
+ pos = pos + 4
+ libname, pos = string.unpack( ">s4", response.body, pos )
+ end
+
+ return libname
+end
+
+--- Reads the first item value specified by name
+--
+-- @param data string containing the unparsed item
+-- @param name string containing the name of the value to read
+-- @return number
+local function getAttributeAsInt( data, name )
+
+ local pos = string.find(data, name)
+ local attrib
+
+ if pos and pos > 0 then
+ pos = pos + 4
+ local len
+ len, pos = string.unpack( ">I4", data, pos )
+
+ if ( len ~= 4 ) then
+ stdnse.debug1("Unexpected length returned: %d", len )
+ return
+ end
+
+ attrib, pos = string.unpack( ">I4", data, pos )
+ end
+
+ return attrib
+
+end
+
+--- Gets the revision number for the library
+--
+-- @param host table containing an ip field.
+-- @param port table containing number and protocol fields.
+-- @return number containing the session identity received from the server
+function getSessionId( host, port )
+
+ local sessionid
+ local response = http.get( host, port, "/login", nil, nil, nil )
+
+ if response ~= nil then
+ sessionid = getAttributeAsInt( response.body, "mlid")
+ end
+
+ return sessionid
+end
+
+--- Gets the revision number for the library
+--
+-- @param host table containing an ip field.
+-- @param port table containing number and protocol fields.
+-- @param sessionid number containing session identifier from <code>getSessionId</code>
+-- @return number containing the revision number for the library
+function getRevisionNumber( host, port, sessionid )
+ local url = "/update?session-id=" .. sessionid .. "&revision-number=1"
+ local revision
+ local response = http.get( host, port, url, nil, nil, nil )
+
+ if response ~= nil then
+ revision = getAttributeAsInt( response.body, "musr")
+ end
+
+ return revision
+end
+
+--- Gets the database identity for the library
+--
+-- @param host table containing an ip field.
+-- @param port table containing number and protocol fields.
+-- @param sessionid number containing session identifier from <code>getSessionId</code>
+-- @param revid number containing the revision id as retrieved from <code>getRevisionNumber</code>
+function getDatabaseId( host, port, sessionid, revid )
+ local url = "/databases?session-id=" .. sessionid .. "&revision-number=" .. revid
+ local response = http.get( host, port, url, nil, nil, nil )
+ local miid
+
+ if response ~= nil then
+ miid = getAttributeAsInt( response.body, "miid")
+ end
+
+ return miid
+end
+
+--- Gets a string item type from data
+--
+-- @param data string starting with the 4-bytes of length
+-- @param pos number containing offset into data
+-- @return pos number containing new position after reading string
+-- @return value string containing the string item that was read
+local function getStringItem( data, pos )
+ local item, pos = string.unpack(">s4", data, pos)
+ return pos, item
+end
+
+local itemFetcher = {}
+
+itemFetcher["mikd"] = function( data, pos ) return getStringItem( data, pos ) end
+itemFetcher["miid"] = itemFetcher["mikd"]
+itemFetcher["minm"] = itemFetcher["mikd"]
+itemFetcher["asal"] = itemFetcher["mikd"]
+itemFetcher["asar"] = itemFetcher["mikd"]
+
+--- Parses a single item (mlit)
+--
+-- @param data string containing the unparsed item starting at the first available tag
+-- @param len number containing the length of the item
+-- @return item table containing <code>mikd</code>, <code>miid</code>, <code>minm</code>,
+-- <code>asal</code> and <code>asar</code> when available
+parseItem = function( data, len )
+
+ local pos, name, value = 1, nil, nil
+ local item = {}
+
+ while( len - pos > 0 ) do
+ name, pos = string.unpack( "c4", data, pos )
+
+ if itemFetcher[name] then
+ pos, item[name] = itemFetcher[name](data, pos )
+ else
+ stdnse.debug1("No itemfetcher for: %s", name)
+ break
+ end
+
+ end
+
+ return item
+
+end
+
+--- Request and process all music items
+--
+-- @param host table containing an ip field.
+-- @param port table containing number and protocol fields.
+-- @param sessionid number containing session identifier from <code>getSessionId</code>
+-- @param dbid number containing database id from <code>getDatabaseId</code>
+-- @param limit number containing the maximum amount of songs to return
+-- @return table containing the following structure [artist][album][songs]
+function getItems( host, port, sessionid, revid, dbid, limit )
+ local meta = "dmap.itemid,dmap.itemname,dmap.itemkind,daap.songalbum,daap.songartist"
+ local url = "/databases/" .. dbid .. "/items?type=music&meta=" .. meta .. "&session-id=" .. sessionid .. "&revision-number=" .. revid
+ local response = http.get( host, port, url, nil, nil, nil )
+ local item, data, pos, len
+ local items = {}
+ local limit = limit or -1
+
+ if response == nil then
+ return
+ end
+
+ -- get our position to the list of items
+ pos = string.find(response.body, "mlcl")
+ pos = pos + 4
+
+ while ( pos > 0 and pos + 8 < response.body:len() ) do
+
+ -- find the next single item
+ pos = string.find(response.body, "mlit", pos)
+ pos = pos + 4
+
+ len, pos = string.unpack( ">I4", response.body, pos )
+
+ if ( pos < response.body:len() and pos + len < response.body:len() ) then
+ data, pos = string.unpack( "c" .. len, response.body, pos )
+ else
+ break
+ end
+
+ -- parse a single item
+ item = parseItem( data, len )
+
+ local album = item.asal or "unknown"
+ local artist= item.asar or "unknown"
+ local song = item.minm or ""
+
+ if items[artist] == nil then
+ items[artist] = {}
+ end
+
+ if items[artist][album] == nil then
+ items[artist][album] = {}
+ end
+
+ if limit == 0 then
+ break
+ elseif limit > 0 then
+ limit = limit - 1
+ end
+
+ table.insert( items[artist][album], song )
+
+ end
+
+
+ return items
+
+end
+
+
+action = function(host, port)
+
+ local limit = tonumber(nmap.registry.args.daap_item_limit) or 100
+ local libname = getLibraryName( host, port )
+
+ if libname == nil then
+ return
+ end
+
+ local sessionid = getSessionId( host, port )
+
+ if sessionid == nil then
+ return stdnse.format_output(true, "Libname: " .. libname)
+ end
+
+ local revid = getRevisionNumber( host, port, sessionid )
+
+ if revid == nil then
+ return stdnse.format_output(true, "Libname: " .. libname)
+ end
+
+ local dbid = getDatabaseId( host, port, sessionid, revid )
+
+ if dbid == nil then
+ return
+ end
+
+ local items = getItems( host, port, sessionid, revid, dbid, limit )
+
+ if items == nil then
+ return
+ end
+
+ local albums, songs, artists, results = {}, {}, {}, {}
+
+ table.insert( results, libname )
+
+ for artist, v in pairs(items) do
+ albums = {}
+ for album, v2 in pairs(v) do
+ songs = {}
+ for _, song in pairs( v2 ) do
+ table.insert( songs, song )
+ end
+ table.insert( albums, album )
+ table.insert( albums, songs )
+ end
+ table.insert( artists, artist )
+ table.insert( artists, albums )
+ end
+
+ table.insert( results, artists )
+ local output = stdnse.format_output( true, results )
+
+ if limit > 0 then
+ output = output .. string.format("\n\nOutput limited to %d items", limit )
+ end
+
+ return output
+
+end