Opinions expressed here belong to your're mom

GPL Praxis: How To Get OpenProject 13 Enterprise Edition (For Free)






OpenProject is a Libre project management application. The primary interface for the application is through the web browser, but a terminal client is under active development. OpenProject (OP) is sometimes useful for managing projects. Recently a new major version, version 13, was realeased. This new version of the software includes various new features such as time tracking and iCalendar integration.

Previously, I have been disappointed by the fact that some features are locked behind an enterprise license. In this post I will explore reverse engineering OP and removing these restrictions without paying. There are a few motivations:

"Reverse engineering" may not be the most accurate term here since I am not decompling anything. However, I am delving into a completely alien codebase in a language that I do not know to try to understand enough to make changes.

Installing OpenProject

I opted to install OP via the officially supported method on Ubuntu 22.04 of just using their repos. The official installation walks through setting up the repository and key and installing the software and then passes off to another section on the initial configuration. I want this as automated as I can get it, so instead of using the installer in interactive mode, I run it in non-interactive mode. This requires that I already have a postgresql database up and running. This isn't a tutorial on PSQL setup so I'm gonna breeze past this. You need:

You also probably want to front OP with Nginx or some other reverse proxy. This will keep you from exposing OP directly to the network and you can manage your SSL certs in one less place. My Nginx config looks like this:

server {
    listen 443 ssl;
    ssl_certificate YOUR_SSL_CERT;
    ssl_certificate_key YOUR_SSL_KEY;
    server_name YOUR_SERVER_FQDN;
    client_max_body_size 100M;
    client_body_buffer_size 128k;
    root /opt/openproject/public;
    location ~ / {
        proxy_pass_request_headers on;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Host $host:$server_port;
        proxy_set_header X-Forwarded-Server $host:$server_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

This will of course return a 502 because OP is not yet set up. To set OP up, you can put a file at /etc/openproject/installer.dat with the contents:

openproject/edition default
postgres/autoinstall reuse
postgres/db_port 5432
postgres/db_username openproject
postgres/db_password DATABASE_PASSWORD_HERE
postgres/db_name openproject
server/autoinstall skip
smtp/autoinstall skip
memcached/autoinstall install
server/hostname YOUR_SERVER_FQDN
server/ssl no
openproject/admin_email example@example.com

And then run the command sudo openproject configure. This will set up OP for you and then it should be listening in the web browser. The default creds are admin/admin and you will be prompted to change this password on your first login. If you have accomplished this then you have met all of the prerequisites for getting OP Enterprise Edition for free.

Tracking Down The Upgrade Functionality

In the webapp, I got a huge annoying banner upon login that I didn't have an enterprise license. This looked like a promising place to start. However, clicking on the "Upgrade Now" button brought me to an external site, which was not what I'm looking for (yet). I instead clicked on the user button in the top right corner ("OA" for "Openproject Admin" by default) and go to Administration and then Enterprise Edition, where I found a page where I would put my "Enterprise edition support token" if I had one.


At first I thought "maybe I can just go to the path /admin/enterprise in the nginx root to find something", but sadly the file /opt/openproject/public/admin/enterprise does not exist (and neither does its parent directory). Looking at the nginx config, the requests are being passed to a process running on port 6000. You can inspect what is actually running on port 6000 like so:

root@openproject:/opt/openproject/public# netstat -tulpane | grep :6000
tcp        0      0*               LISTEN      997        121934     30374/puma 6.3.0 (t
tcp        0      0          TIME_WAIT   0          0          -
tcp        0      0          TIME_WAIT   0          0          -
tcp        0      0          TIME_WAIT   0          0          -

I have no clue what "puma" is but it is the application that is doing the serving, so let's look at that:

root@openproject:/opt/openproject/public# ps -ef | grep puma
openpro+   30374   30318  0 18:12 ?        00:00:11 puma 6.3.0 (tcp:// [openproject]
openpro+   30397   30374  0 18:12 ?        00:00:05 puma: cluster worker 0: 30374 [openproject]
openpro+   30401   30374  0 18:12 ?        00:00:04 puma: cluster worker 1: 30374 [openproject]
root@openproject:/opt/openproject/public# systemctl status | grep puma -B 2
             │ ├─30318 /bin/bash -e ./packaging/scripts/web
             │ ├─30374 puma 6.3.0 (tcp:// [openproject]
             │ ├─30397 puma: cluster worker 0: 30374 [openproject]
             │ └─30401 puma: cluster worker 1: 30374 [openproject]

It looks like the process is being run from the openproject-web-1 systemd service. Looking at the contents of /etc/systemd/system/openproject-web-1.service to see where the actual starting point for the web application is yeilds:

root@openproject:/opt/openproject/public# cat /etc/systemd/system/openproject-web-1.service

ExecStart=/usr/bin/openproject run web


This command in ExecStart is the main OP executable, and thankfully it isn't even a binary, it is just a really long Bash script generated by some tool called "pkgr". In this script I found what happens when using the run subcommand:

while : ; do
  case "$1" in
    exec sh -c "cd $(_p ${APP_HOME}) && $runnable"

That ${APP_HOME} variable is set in /etc/default/openproject to be /opt/openproject. I could have come to this same conclusion by looking at the output of dpkg-query -L openproject and sifting through the files there, but this way was more fun. Looking at some of the other variable setting in this script, I eventually figured out that the binary that gets run by the above exec command is /opt/openproject/vendor/pkgr/processes/web, which itself just launches /opt/openproject/scripts/web, which in turn launches our puma process from earlier:

#!/bin/bash -e


bundle exec rails server -u puma -b $HOST -p $PORT

Everything from here on is Ruby and I was well out of my depth. Ignorance, however, is not a sufficient excuse to not do something. Now that I had found the OP application files, I just needed to find the code that manages the enterprise license. Running the command grep -ril enterprrise . in the /opt/openproject/ directory will show everywhere that that string shows up in the codebase, but there are a lot of results. Of the 843 files containing this string, a lot can be completely ignored.

All of these aren't worth looking at beacuse they are not where the code to import and validate an enterprise token is coming from. Of the original 843 files, this leaves only 9 to actually look through:

  1. ./lib/constraints/enterprise.rb
  2. ./lib/open_project/enterprise.rb
  3. ./vendor/bundle/ruby/3.2.0/gems/openproject-token-3.0.1/lib/open_project/token.rb
  4. ./app/helpers/enterprise_helper.rb
  5. ./app/helpers/enterprise_trial_helper.rb
  6. ./app/services/authorization/enterprise_service.rb
  7. ./app/contracts/concerns/requires_enterprise_guard.rb
  8. ./app/models/enterprise_token.rb
  9. ./app/models/token/enterprise_trial_key.rb

The very first file here just creates a function called "matches" in the "Enterprise" module. The next file, ./lib/open_project/enterprises.rb appears to set some methods that can be used for enterprise tokens . The "token" method checks the EnterpriseToken.current.presence, which is defined in the ./app/models/enterprise_token.rb file. The "current" method of that class calls the RequestStore module and then returns (I think) the content of the key that it is querying, in this case the EE (enterprise edition) token. Grepping for RequestStore.fetch shows a lot of places in the OP code that this is used, so to avoid breaking other functionality, I don't think that this is the best place to make modifications. The ./app/helpers/enterprise_helper.rb, enterprise_trial_helper.rb, and ./app/models/token/enterprise_trial_key.rb files are all used for (I think) creating the metadata for the trial license.

That only leaves three files left. One is the Ruby gem file token.rb which takes in the token data that OpenProject (the company) gives you when you pay them money, validates it, and sets it up in your database. The other two files, which are in the ./app/ directory, look like the place where restrictions and enforcements of what is and is not allowed without an EE token are defined and enforced. This leaves two options with how to move forward:

  1. Disable restrictions so that OP doesn't stop you from doing anything without a token
  2. Modify token validation so that OP doesn't check if your token is legit

Changing The Constraints

The first place that I tried to get around this licensing issue was in the ./app/services/authorization/enterprise_service.rb file. That has some particularly interesting snippets:



  # Return a true ServiceResult if the token contains this particular action.
  def call(action)
    allowed =
      if token.nil? || token.token_object.nil? || token.expired?


Initially, this first section looks like a list of actions that OP won't let you do unless you have an EE token. Those are in fact all of the different places where pieces of the application are guarded behind a paywall. The syntax is something that I hadn't seen before, the %i().freeze formatting being completely alien to me. With some looking around online, it appears that the "freeze" part of this is used to make the variable immutable, which means that it can't be changed after that point in the code. The parentheses give us easy string interpoltaion without needing to worry about quotes. The "%i" part at the beginning converts ths contents of the parentheses into an array of "symbols", which are just immutable strings. So I tried to replace this list of guarded actions like so:

  GUARDED_ACTIONS = %i().freeze

And then restarted openproject (systemctl restart openproject). Doing this did not immeidately allow me to use Kanban boards in the browser interface, which was a bummer. Maybe the second section of code, which appears to check if a token is present or expired, was my real target. This second section of code defines a method named call which takes in an action name, sets a boolean variable named allowed based on whether or not a token is present/absent/expired, and then runs result with that boolean. This final result function goes on to use something else from another module (ServiceResult), but I suspected that the check for token validity was all that I needed to bypass. I tried changing:

    allowed =
      if token.nil? || token.token_object.nil? || token.expired?


    allowed = true

Sadly, this still did not allow me to access enterprise features without a license. At this point I didn't even know for sure if OP was getting to my modified code or not, since my dissection of what this file is for was pure speculation. To test, this, I figured that I would honor the time-tested tradition of debugging with print statements. One quick "how to hello world in Ruby" later, I added a puts "we dem boyz" to the part of the function where I forcefully set allowed to true. Restarting openproject and reloading a kanban page did in fact print many copies of this string to the systemd journal for openproject-web-1.service. However, the restrictions were still in place.

"Maybe I need to look a level deeper at the ServiceResult call" was my next thought. This class is defined in ./app/services/service_result.rb, and it is really long and complicated, but it does fortunately have two lines at the top that look like they might be very impactful:

  SUCCESS = true
  FAILURE = false

So I changed the false for failure to a true. Sadly, I still got a "please buy OP EE" message when I wanted to just move rectangles around three columns with my mouse. Some functions, however, I was able to use, such as the team planner and site themeing. Looking instead at one of the actual restrictions in the code, I see a chain of calls to various other classes and methods just to check if some action is allowed:


    def restricted_board_type?
      !EnterpriseToken.allows_to?(:board_view) && board_grid_params[:attribute] != 'basic'


class EnterpriseToken < ApplicationRecord
  class << self
    def current
      RequestStore.fetch(:current_ee_token) do

    def table_exists?
      connection.data_source_exists? table_name

    def allows_to?(action)

    def active?
      current && !current.expired?

    def show_banners?
      OpenProject::Configuration.ee_manager_visible? && !active?

    def set_current_token
      token = EnterpriseToken.order(Arel.sql('created_at DESC')).first

      if token&.token_object

"Maybe I can force the changes in enterprise_token.rb", I thought. I set the functions table_exists, allows_to, and active to just be true, I set show_banners to false because I don't want to see the banners in the web application, and I didn't modify the set_current_token because it looked too complicated. This actually allowed me to get the Kanban boards working in the web browser. This is all well and good but modifications spread across several different files with unknown implications elsewhere is not really what I was going for when I set out on this path. The real best solution would be a clean solution. After some tinkering to see how much I could possibly reduce my modifications, the actual modifications that matter are:


    def table_exists?
      #connection.data_source_exists? table_name

    def allows_to?(action)

    def active?
      #current && !current.expired?

    def show_banners?
      #OpenProject::Configuration.ee_manager_visible? && !active?

Making just these changes allows me to access the themeing in the administration view and the Kanban boards. Technically, this meets my original desires for getting EE features without paying. However, this isn't a great solution. Here I am replacing a bunch of different lines of code (after writing this blogpost I discovered that I was not the first person to think of this). However, I worried that there could be soemthing that I was missing since it seemed like OP checked in multiple places for token validity. What if I could trick OP into thinking that I had a perfectly valid key? Then I could let all of the key validity checks keep going without worry, since OP would think that the key is totally valid.

Spoofing A Key

When you attempt to import a token into OpenProject on the /admin/enterprise page, the software looks at the token and makes sure that it is legit, then it grants you the EE features. This is done in the file ./vendor/bundle/ruby/3.2.0/gems/openproject-token-3.0.1/lib/open_project/token.rb, provided by this Ruby gem. When you attempt to add a new license, the software checks that it is in fact a license file, checks that it is from the OP Foundation, and sets various values about user count and expiration. To assist in debugging this script, I got a free trial version of the license, which lasts for 2 weeks. This is plenty of time to figure out how to remove such silly checks. In order to get a trial license, you can just go to this page. Keep in mind that you need a verified email address.

get the useful information out of this token, I added a piece to the code that writes the data from my temporary trial license to a file. I added this just after the final JSON is acquired:

** ./vendor/bundle/ruby/3.2.0/gems/openproject-token-3.0.1/lib/open_project/token.rb**

out_file = File.new("/tmp/op_json.txt", "w")

Then I imported the token. This wrote the file mentioned above, which had plaintext JSON data:

{"version":"2.0","subscriber":"Firstname Lastname","mail":"someemail@domain.tld","company":"Company Name","domain":"openproject.domain.tld","issued_at":"2023-09-31","starts_at":"2023-09-31","expires_at":"2023-12-52","reprieve_days":14,"restrictions":{"active_user_count":10}}

And that looks like something that I can edit. Modifying this JSON and replacing the code with a static declaration of my very legitimate license:

** ./vendor/bundle/ruby/3.2.0/gems/openproject-token-3.0.1/lib/open_project/token.rb**

        #data = Armor.decode(data)
        #json = extractor.read(data)
    json = '{"version": "1.9","subscriber": "Richard Stallman","mail": "rms@gnu.org","issued_at": "1970-01-01","starts_at": "1970-01-01","expires_at": "2970-01-01","reprieve_days": 14,"restrictions": {"active_user_count": 1000}}'
    attributes = JSON.parse(json)

The different options here all have reasoning behind them:

After making this change and restarting OP, I went to the page where you can enter a new enterprise key. Entering the letter a and clicking the "Save" button did now show the fake license and unlocked all enterprise features. This was an improvement to the previous solution because it required editing less lines of source code. However, this still required modifying the actual source code files of OP, which could be updated to require different workarounds in the future, increasing the amount of work that I have to do when OP 14 comes out if they completely rework the token validation code. At this point I had stars in my eyes and a dream in my head. Is there a way to do accomplish this task without editing any source code files from OP? Would it be possible to make my own completely valid token from scratch? The only way to find out would be to dive into the actual token extraction process.

The Enterprise Token

The actual token license key looks like this:


Which looks a lot like a PGP ASCII-armored message. Changing the header and footer of the file from OPENPROJECT-EE TOKEN to PGP MESSAGE did not allow me to gpg --deamor ee-token.asc, however. Luckily, I know base64 when I see it. Taking out the middle part and putting it into a base64 decoder, I got some JSON output with plaintext (JSON) keys and (JSON) values that are again base64 encoded:

    "data": "SSdtIG5vdCBzaG93aW5nIHlvdSBsb2wK\n",
    "key": "dGhlIGluZHVzdHJpYWwgY29uc2VxdWVuY2VzIGFuZCBpdHMgZXRjCkxpc3RlbiBtYW4gZG8geW91IGV2ZXIgZ2V0IHRpcmVkIG9mIHNlZWluZyB0aGUgc2FtZSBpZGVhcyBvbmxpbmUgYWxsIHRoZSB0aW1lPyBNYW5raW5kIHdhc24ndCBtZWFudCB0byBoYXZlIHRoZSBzYW1lIHRob3VnaHRzIGV2ZXJ5IGRheSBmb3JldmVyLiBJIGRvbid0IHRoaW5rIHRoYXQgY2FuIGJlIG9yZ2FuaWMgb3IgaGVhbHRoeSBmb3IgdXMuIE1heWJlIGl0IHdvdWxkIGJlIGEgZ29vZCBpZGVhIHRvIGN1dCBvZmYgZXh0ZXJuYWwgc291cmNlcyBvZiB0aG91Z2h0LiBJIGRvbid0IGtub3csIEknbSBqdXN0IGEgZHVkZSB3aXRoIGEga2V5Ym9hcmQu=\n"
    "iv": "c2hvcnR5==\n"

But when I plugged those values into a base64 decoder, I got binary gibberish. Judging by the contents of vendor/bundle/ruby/3.2.0/gems/openproject-token-3.0.1/lib/open_project/token/extractor.rb, this is because these values are either encrypted or binary representations of the data. The "data" section is the actual stuff that I want, the "key" section is the decryption key for that data, and the initialization vector is the "aes_iv":


        encrypted_data  = Base64.decode64(encryption_data["data"])
        encrypted_key   = Base64.decode64(encryption_data["key"])
        aes_iv          = Base64.decode64(encryption_data["iv"])

And that AES encrypted key is itself actually encrypted with an RSA key. AES is symmetric and RSA is asymmetric. The RSA public key is needed to get the AES key out. The openproject-token gem actually just calls out to the openssl library, using the verify_recover function via the backwards compatibility of the deprecated "public_decrypt" function.


          # Decrypt the AES key using asymmetric RSA encryption.
          aes_key = self.key.public_decrypt(encrypted_key)
        rescue OpenSSL::PKey::RSAError
          raise DecryptionError, "AES encryption key could not be decrypted."

        # Decrypt the data using symmetric AES encryption.
        cipher = OpenSSL::Cipher::AES128.new(:CBC)


    def public_decrypt(string, padding = PKCS1_PADDING)
      n or raise OpenSSL::PKey::RSAError, "incomplete RSA"
        verify_recover(nil, string, {
          "rsa_padding_mode" => translate_padding_mode(padding),
      rescue OpenSSL::PKey::PKeyError
        raise OpenSSL::PKey::RSAError, $!.message

At this point I had to figure out where the RSA public/private keypair comes from. The extractor.rb file gets its RSA key from the token.rb file, which gets it from:

** ./vendor/bundle/ruby/3.2.0/gems/openproject-token-3.0.1/lib/open_project/token.rb**

      def key=(key)
        if key && !key.is_a?(OpenSSL::PKey::RSA)
          raise ArgumentError, "Key is missing."

        @key = key
        @extractor = Extractor.new(self.key)

But I had no idea where the "key" here came from. I assumed probably that it was passed to the Token object when it is defined, so if I could find the location in the OP code where that happens, maybe I could find the key. By doing a recursive grep for [^a-z]Token and looking through the results similar to how I found the original relevant files for the enterprise licnse itself, I was able to track down that the RSA key was pulled in from the disk:


  data = File.read(Rails.root.join(".openproject-token.pub"))
  key = OpenSSL::PKey::RSA.new(data)
  OpenProject::Token.key = key
rescue StandardError
  warn "WARNING: Missing .openproject-token.pub key"

It can be a bit difficult to fully wrap your head around what all is going on here, so I wrote a script that imitates OP's functionality to go from an "OPENPROJECT-EE TOKEN" to actual usable license JSON:

#!/usr/bin/env ruby

require "base64"
require "openssl"
require "json"

token_file = 'REAL.token'
pub_key = 'REAL.pub'

# Read in Token text
ee_token = File.read token_file

# Remove header and footer
header = "-----BEGIN OPENPROJECT-EE TOKEN-----"
footer = "-----END OPENPROJECT-EE TOKEN-----"
match = ee_token.match /#{header}\r?\n(.+?)\r?\n#{footer}/m

# Base64 decode string
json_data = Base64.decode64(match[1].chomp)

# Parse JSON
encryption_data = JSON.parse(json_data)

# Base64-decode RSA-encrypted AES key, AES IV, and AES-encrypted License
encrypted_data = Base64.decode64(encryption_data['data'])
encrypted_key = Base64.decode64(encryption_data['key'])
aes_iv = Base64.decode64(encryption_data['iv'])

# RSA decrypt AES key
rsa_public_key = OpenSSL::PKey::RSA.new File.read pub_key
aes_key = rsa_public_key.public_decrypt(encrypted_key)

# AES decrypt the license
cipher = OpenSSL::Cipher::AES128.new(:CBC)
cipher.key = aes_key
cipher.iv = aes_iv
data = cipher.update(encrypted_data) + cipher.final

# Parse License JSON
license = JSON.parse(data)
puts license

Step by step, this script exactly imitates what OP does:

  1. Read in the token data sent via email from the OP Foundation
  2. Strip off the header and footer
  3. Convert the base64 text from the token to JSON
  4. Use the OP Foundation's public RSA key to decrypt the AES key
  5. Use the decrypted AES key and IV to decrypt the license text
  6. Parse the license text to JSON

Writing this script was very helpful in the process of fully understanding what OP is doing at each step, and it helped me learn a little bit of Ruby. This script requires two files in the current working directory:

Since asymmetrical encryption is used to secure the AES key, I needed the private key in order to create and sign my own EE tokens. I assume that this private key is kept safe on the OP Foundation's servers and cannot be accessed by regular people, as that would allow for exactly what I am trying to do here. Probably this file is named openproject-token.key and the contents look something like this:


I looked (not very hard) but couldn't locate an accidentally-public private key with the regular search engines. This means that a universal key for all OP installations is not possible (until some point when such a key can be found). But if I replace the public key file with my own public key, then I should be able to generate my own license from it.

Recreating The Token

The structure of the token is like so:


This seems pretty complicated at first glance. Certainly it is more complicated than it needs to be. This could be as simple as

I have no idea why this extra step of AES encryption is used, execept possibly as a deterrance for people reverse engineering the token.

To start, I needed to create my own RSA public/private keypair, since the OP Foundation was not interested in sharing their private copy (I'm just assuming here, since I didn't ask). This can be done pretty easily on Linux with the commands:

openssl genrsa -out private.rsa 4096
openssl rsa -in private.rsa -pubout -out public.rsa

Which creates two files in the CWD, private.rsa for the private key and public.rsa for the public key. The public.rsa key needs to go in the OP installation directory. On Ubuntu server, the location that it needs to go is /opt/openproject/.openproject-token.pub. I replaced the file that was there with this newly generated one. After doing this, the next steps (in order) are:

  1. AES-encrypt the license
  2. RSA-encrypt the AES-encryption key
  3. Base64-encoded the AES-encrypted license, RSA-encrypted AES key, and the AES IV
  4. Store those encoded versions in JSON
  5. Base64-encode that JSON
  6. Slap a header and footer on that encoded JSON

I tried a few ways to do this directly on the shell, but I ran into issues when trying to Base64-encode the AES IV. This was becasue the IV is a hexadecimal number and when I cat $iv | base64, that Base64-encodes the ASCII version of the string, not the hex version of the string. Eventually I bit the bullet and wrote a Ruby script inspired by OP itself to do the heavy lifting:

#!/usr/bin/env ruby

require 'base64'
require 'openssl'
require 'json'

# License Plaintext
license_plaintext = '{"version": "1.9","subscriber": "Richard Stallman","mail": "rms@gnu.org","issued_at": "1970-01-01","starts_at": "1970-01-01","expires_at": "2970-01-01","reprieve_days": 14,"restrictions": {"active_user_count": 1000}}'

# AES encryption setup
aes = OpenSSL::Cipher::AES128.new(:CBC)
aes_key = aes.random_key
aes_iv = aes.random_iv
aes.key = aes_key
aes.iv = aes_iv

# Encrypt license with AES
aes_encrypted_license = aes.update(license_plaintext)
aes_encrypted_license << aes.final

# RSA encryption stuff
rsa_private_key = OpenSSL::PKey::RSA.new File.read 'private.rsa'

# Encrypt AES key with RSA
rsa_aes_key = rsa_private_key.private_encrypt(aes_key)

# Base64 encode RSA-encrypted AES key, AES IV, and AES-encrypted License
encoded_license = Base64.encode64(aes_encrypted_license)
encoded_key = Base64.encode64(rsa_aes_key)
encoded_iv = Base64.encode64(aes_iv)

# Make a JSON out of the above data
json_data = JSON[{
  'data': encoded_license,
  'key': encoded_key,
  'iv': encoded_iv

# Base64 encode the JSON
data = Base64.encode64(json_data)

# Print with header and footer
puts data

Running this code spits out a working OP license which grants unlimited enterprise features.


How To Use This Hack

If you self-host OpenProject, you can remove those annoying banners and get Enterprise Edition for free by following this 2-step process:

  1. curl -s https://punkto.org/no_cdn_lol/openproject/ruby/public.rsa | sudo tee /var/openproject/.openproject-token.pub && sudo systemctl restart openproject
  2. Go to the page on your installation to enter an enterprise license. This is the domain.tld/admin/enterprise page by default and paste in this key:

There you have it, a simple working method to get a never-ending OP enterprise license. This requires no modifications to the OP source code, so this should drastically lower the barrier of entry to free OP for users around the globe.

Hackers need to pay their bills, too. By donating Monero to 89zUPdtgXvo268ZtbzE7LBTEdo2Y75V1FUSs1rETeaRudQZy8BRs4rwTQuFzWqkQ6VU14Ei5TUBGhYCY6TmbCoEvQ2YkqC9, you will be supporting this effort and contributing to future development, maintenance, and continuous improvement.


This page is being served digitally. For a physical copy, print it off

Dancing Baby Seal Of Approval Webserver Callout My Text Editor Bash Scripting 4 Lyfe yarg Blog RSS Feed