# encoding: UTF-8
require 'rubygems'
require 'net/https'
require 'json'
require 'zip'
require 'json'

# Authors:
# Michele '@antisnatchor' Orru
# Krzysztof Kotowicz - @kkotowicz
# README:
# Before running the script, make sure you change the following 4 variables in config.rb:
# G_PUBLISHER_ID, SID, SSID, HSID
#
# You can retrieve all these values after you're successfully authenticated in the WebStore, see comments
# in the config.rb.sample. Rename it ro config.rb when done.
#

require_relative 'config.rb'

def help()
    puts "[-] Error. Usage: ruby webstore_upload.rb <zip_file_name> <publish|save> [description_file] [screenshot_file]"
    exit 1
end

zip_name = ARGV[0]
if zip_name == nil
    help()
end
EXT_ZIP_NAME = zip_name

mode = ARGV[1]
action = nil
if mode == nil
    help()
elsif mode == "publish"
    action = "publish"
elsif mode == "save"
    action = "save_and_return_to_dashboard"
    #action = "save"
else
    help()
end
ACTION = action

if !File.exists?(EXT_ZIP_NAME)
    puts "[-] Error: #{EXT_ZIP_NAME} does not exist"
    help()
end

if ARGV[2] != nil and File.exists?(ARGV[2])
    DESCRIPTION = File.new(ARGV[2]).read()
    puts "[*] Using description from #{ARGV[2]}"
else
    DESCRIPTION = ""
end

if ARGV[3] != nil and File.exists?(ARGV[3])
    SCREENSHOT_NAME = ARGV[3]
    puts "[*] Using screenshot from #{SCREENSHOT_NAME}"
end

#  general get/post request handler
def request(uri, method, headers, post_body)
    uri = URI(uri)
    http = nil
    if USE_PROXY
        http = Net::HTTP.new(uri.host, uri.port, PROXY_HOST, PROXY_PORT)
    else
        http = Net::HTTP.new(uri.host, uri.port)
    end

    http.read_timeout = HTTP_READ_OPEN_TIMEOUT
    http.open_timeout = HTTP_READ_OPEN_TIMEOUT
    if uri.scheme == "https"
        http.use_ssl = true
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    end

    request = nil
    if method == "POST"
        request = Net::HTTP::Post.new(uri.request_uri, headers)
        if post_body.is_a?(Hash)
            request.set_form_data(post_body) # post_body as in: {"key" => "value"}
        else
            request.body = post_body # post_body as in: {"key" => "value"}
        end
    else # otherwise GET
        request = Net::HTTP::Get.new(uri.request_uri, headers)
    end

    begin
        response = http.request(request)

        case response
            when Net::HTTPSuccess
            then
                return response
            when Net::HTTPRedirection # if you get a 3xx response
            then
                return response
            else
                return nil
        end
    rescue SocketError => se # domain not resolved
        return nil
    rescue Timeout::Error => timeout # timeout in open/read
        return nil
    rescue Errno::ECONNREFUSED => refused # connection refused
        return nil
    rescue Exception => e
       #puts e.message
       #puts e.backtrace
        return nil
    end
end

# raw request to upload the extension.zip file data
def request_octetstream(uri, headers)
    file = File.new(EXT_ZIP_NAME)

    uri = URI(uri)
    req  = Net::HTTP::Post.new(uri.request_uri, headers)

    post_body = []
    post_body << File.read(file)
    req.body = post_body.join
    req["Content-Type"] = "application/octet-stream"

     http = nil
     if USE_PROXY
         http = Net::HTTP.new(uri.host, uri.port, PROXY_HOST, PROXY_PORT)
     else
         http = Net::HTTP.new(uri.host, uri.port)
     end

     http.read_timeout = HTTP_READ_OPEN_TIMEOUT
     http.open_timeout = HTTP_READ_OPEN_TIMEOUT
     if uri.scheme == "https"
      http.use_ssl = true
      http.verify_mode = OpenSSL::SSL::VERIFY_NONE
     end
     response = http.request(req)
     return response
end

# defaults to English Language and Productivity category
def send_publish_request(uri, cx_auth_t, headers, action="publish")

    uri = URI(uri)
    req  = Net::HTTP::Post.new(uri.request_uri, headers)
    boundary = rand(14666338978986066131776338987).to_s.center(29, rand(29).to_s)

    fields = {
        "action" => action, "t" => cx_auth_t, "edit-locale" => "en_US", "desc" => DESCRIPTION, "screenshot" => SCREENSHOT_NAME, "cx-embed-box" => "",
        "official_url" => "none", "homepage_url" => "", "support_url" => "", "categoryId" => "7-productivity", "tiers" => "0", "all-regions" => "1",
        "cty-AR" => "1", "cty-AU" => "1", "cty-AT" => "1", "cty-BE" => "1", "cty-BR" => "1", "cty-CA" => "1", "cty-CN" => "1",
        "cty-CZ" => "1", "cty-DK" => "1", "cty-EG" => "1", "cty-FI" => "1", "cty-FR" => "1", "cty-DE" => "1", "cty-HK" => "1",
        "cty-IN" => "1", "cty-ID" => "1", "cty-IL" => "1", "cty-IT" => "1", "cty-JP" => "1", "cty-MY" => "1", "cty-MX" => "1",
        "cty-MA" => "1", "cty-NL" => "1", "cty-NZ" => "1", "cty-NO" => "1", "cty-PH" => "1", "cty-PL" => "1", "cty-PT" => "1",
        "cty-RU" => "1", "cty-SA" => "1", "cty-SG" => "1", "cty-ES" => "1", "cty-SE" => "1", "cty-CH" => "1", "cty-TW" => "1",
        "cty-TH" => "1", "cty-TR" => "1", "cty-UA" => "1", "cty-AE" => "1", "cty-GB" => "1", "cty-US" => "1", "cty-VN" => "1",
        "language" => "en", "openid_realm" => "", "analytics_account_id" => "", "extensionAdsBehavior" => "", "publish-destination" => "PUBLIC",
        "ignore" => "true", "payment-type" => "free", "subscription-period" => "none", "logo128-image" => "",
    }

    post_body = []
    post_body << "-----------------------------#{boundary}\r\n"
    fields.each do |key,value|
        if key == "screenshot"
            # screenshot must be treated differently
            post_body << "Content-Disposition: form-data; name=\"#{key}\"; filename=\"" + (value ? File.basename(value) : '' )+ "\"\r\n"
            post_body << "Content-Type: application/octet-stream\r\n\r\n"
            post_body << (value ? File.read(value) : '')
            post_body << "\r\n"
            post_body << "-----------------------------#{boundary}\r\n"
            next
        elsif key == "logo128-image"
            post_body << "Content-Disposition: form-data; name=\"#{key}\"; filename=\"" + (ICON_NAME ? File.basename(ICON_NAME) : '') + "\"\r\n"
            post_body << "Content-Type: application/octet-stream\r\n\r\n"
            post_body << ICON
            post_body << "\r\n"
            post_body << "-----------------------------#{boundary}\r\n"
            next
        end
        post_body << "Content-Disposition: form-data; name=\"#{key}\"\r\n"
        post_body << "\r\n"
        post_body << "#{value}\r\n"
        if key == "logo128-image"
            post_body << "-----------------------------#{boundary}--\r\n"
            break
        else
            post_body << "-----------------------------#{boundary}\r\n"
        end
    end

    req.body = post_body.join


    req["Content-Type"] = "multipart/form-data; boundary=---------------------------#{boundary}"

    http = nil
    if USE_PROXY
        http = Net::HTTP.new(uri.host, uri.port, PROXY_HOST, PROXY_PORT)
    else
        http = Net::HTTP.new(uri.host, uri.port)
    end

    http.read_timeout = HTTP_READ_OPEN_TIMEOUT
    http.open_timeout = HTTP_READ_OPEN_TIMEOUT
    if uri.scheme == "https"
        http.use_ssl = true
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    end
    response = http.request(req)
    return response
end

puts "[+] Reading manifest..."
Zip::File.open(EXT_ZIP_NAME) do |zip_file|
    entry = zip_file.glob('manifest.json').first
    manifest = JSON.parse(entry.get_input_stream.read)
    ICON_NAME = manifest['icons']['128']
    puts "[+] Found icon: #{ICON_NAME}"
    ICON = zip_file.glob(ICON_NAME).first.get_input_stream.read
    ICON.force_encoding 'utf-8'
end

upload_url = "https://chrome.google.com/webstore/developer/upload"
post_body = '{"protocolVersion":"0.8","createSessionRequest":{"fields":[{"external":{"name":"file","filename":'+
'"' + File.basename(EXT_ZIP_NAME) + '","put":{},"size":' + File.new(EXT_ZIP_NAME).size.to_s + '}},{"inlined":{"name":"extension_id","content":'+
'"null","contentType":"text/plain"}},{"inlined":{"name":"package_id","content":"main","contentType":"text/plain"}},'+
'{"inlined":{"name":"publisher_id","content":"' + G_PUBLISHER_ID + '","contentType":"text/plain"}},'+
'{"inlined":{"name":"language_code","content":"en-US","contentType":"text/plain"}}]}}'

auth_headers = {'Cookie'=> "SID=#{SID}; HSID=#{HSID}; SSID=#{SSID};"}

upload_auth_resp = request(upload_url, 'POST', auth_headers, post_body)
upload_status = JSON.parse(upload_auth_resp.body)
if upload_status['errorMessage'] == nil
    upload_id = upload_status['sessionStatus']['upload_id']
    upload_url = "https://chrome.google.com/webstore/developer/upload?upload_id=#{upload_id}&file_id=000"

    puts "[+] Uploading ZIP..."
    response = request_octetstream(upload_url, auth_headers)

    upload_status = JSON.parse(response.body)
    if upload_status['errorMessage'] == nil && upload_status['sessionStatus']['state'] == "FINALIZED"
        extension_id = upload_status['sessionStatus']['additionalInfo']['uploader_service.GoogleRupioAdditionalInfo']['completionInfo']['customerSpecificInfo']['extension_id']
        puts "[+] Extension uploaded successful. Extension ID: #{extension_id}"

        # Last request, to Publish the extension, requires Language/Category to be set.
        # A multipart/form-data request is sent, but we first need to get an hidden form field "cx-action-t" value,
        # then send the final multipart/form-data request with that value inside.
        puts "[+] Fetching edit page..."
        edit_ext_url = "https://chrome.google.com/webstore/developer/edit/#{extension_id}"
        edit_ext_resp = request(edit_ext_url, 'GET', auth_headers, nil)

        cx_action_t = edit_ext_resp.body.split("id=\"cx-action-t\" name=\"t\" value=\"").last.split("\"").first
        if cx_action_t.index('<') != nil # error
            puts ['[-] Error: Session invalid, update cookies values']
            exit 1
        end
        puts "[+] Retrieved cx-action-t hidden field value: #{cx_action_t}"
        puts "[+] Sending #{ACTION} request..."
        edit_ext_resp = send_publish_request(edit_ext_url, cx_action_t, auth_headers, ACTION)
        
        if edit_ext_resp.is_a?(Net::HTTPRedirection)
            puts "[+] Extension details (category/language) updated."
            final_location = edit_ext_resp['Location']
            if ACTION == 'publish'
                puts "[+] Extension is in queue for publishing. URL: https://chrome.google.com#{final_location}"
            else
                puts "[+] Extension updated. URL: https://chrome.google.com#{final_location}"
            end
        else
            if edit_ext_resp.body and edit_ext_resp.body.include?('Please fix the following errors:<ul>')
                errors = edit_ext_resp.body.split("Please fix the following errors:<ul>").last.split("</ul>").first.gsub(/<[^>]*>/ui,' ')
                puts "[-] Errors: #{errors}"
            end
            puts "[-] Error updating extension details. Anyway, the extension is uploaded."
        end
    else
        puts "[-] Error: #{upload_status['errorMessage']}"
    end

else
    puts "[-] Error: #{upload_status['errorMessage']}"
end
